kaeruspoon

AWDwR2を買う

RailsによるアジャイルWebアプリケーション開発 第2版

 風邪をひいてしまった。せっかく「RailsによるアジャイルWebアプリケーション開発 第2版」を買ったのだけど、頭が痛くて内容が入ってこない。パラ読みした感じではよさそうでワクワクしています。

Rails2.0のインストール

 Rails2.0のPRが出たので、いろいろと試してみました。まずはインストール。

sudo gem install rails --source http://gems.rubyonrails.org

あとは適当にrailsアプリを作って、Rails2.0PRをfreezeします。

rails rails2_test
rake rails:freeze:edge TAG=rel_2-0-0_PR

これで vender/railsにRails2.0PRが入れられて、rails2_testアプリはここのRailsを使うようになります。

map.resourcesを試してみる

Userモデルと、index(一覧表示)、show(ひとつの表示)、new,create(新規作成)、edit,update(更新)、destroy(削除)のアクションを持つコントローラがあったとすると、config/routes.rbに

map.resources :users

と書くだけでカッコいいURIを自動で作ってくれます。その確認はrake routesコマンドでOK。

              users GET    /users                           {:action=>"index", :controller=>"users"}
    formatted_users GET    /users.:format                   {:action=>"index", :controller=>"users"}
                    POST   /users                           {:action=>"create", :controller=>"users"}
                    POST   /users.:format                   {:action=>"create", :controller=>"users"}
           new_user GET    /users/new                       {:action=>"new", :controller=>"users"}
 formatted_new_user GET    /users/new.:format               {:action=>"new", :controller=>"users"}
          edit_user GET    /users/:id/edit                  {:action=>"edit", :controller=>"users"}
formatted_edit_user GET    /users/:id/edit.:format          {:action=>"edit", :controller=>"users"}
               user GET    /users/:id                       {:action=>"show", :controller=>"users"}
     formatted_user GET    /users/:id.:format               {:action=>"show", :controller=>"users"}
                    PUT    /users/:id                       {:action=>"update", :controller=>"users"}
                    PUT    /users/:id.:format               {:action=>"update", :controller=>"users"}
                    DELETE /users/:id                       {:action=>"destroy", :controller=>"users"}
                    DELETE /users/:id.:format               {:action=>"destroy", :controller=>"users"}

 左から、名前、メソッド、URI、実際のアクション、になります。/usersに対してGETメソッドでアクセスすると一覧表示、同じURIに対してPOSTメソッドでデータを送ると、新しいモデルオブジェクトが作成されます。/users/:idに対してGETメソッドでひとつのデータの表示、PUTメソッドで更新、DELETEメソッドで削除されます。RESTfulでカッコいい。

 さらにUserモデルに1対多でひもつくCarモデルがあった場合、config/routes.rbに

 map.resources :users, :has_many => :cars

と書いてやることで、

.....省略......
              user_cars GET    /users/:user_id/cars                  {:action=>"index", :controller=>"cars"}
    formatted_user_cars GET    /users/:user_id/cars.:format          {:action=>"index", :controller=>"cars"}
                        POST   /users/:user_id/cars                  {:action=>"create", :controller=>"cars"}
                        POST   /users/:user_id/cars.:format          {:action=>"create", :controller=>"cars"}
           new_user_car GET    /users/:user_id/cars/new              {:action=>"new", :controller=>"cars"}
 formatted_new_user_car GET    /users/:user_id/cars/new.:format      {:action=>"new", :controller=>"cars"}
          edit_user_car GET    /users/:user_id/cars/:id/edit         {:action=>"edit", :controller=>"cars"}
formatted_edit_user_car GET    /users/:user_id/cars/:id/edit.:format {:action=>"edit", :controller=>"cars"}
               user_car GET    /users/:user_id/cars/:id              {:action=>"show", :controller=>"cars"}
     formatted_user_car GET    /users/:user_id/cars/:id.:format      {:action=>"show", :controller=>"cars"}
                        PUT    /users/:user_id/cars/:id              {:action=>"update", :controller=>"cars"}
                        PUT    /users/:user_id/cars/:id.:format      {:action=>"update", :controller=>"cars"}
                        DELETE /users/:user_id/cars/:id              {:action=>"destroy", :controller=>"cars"}
                        DELETE /users/:user_id/cars/:id.:format      {:action=>"destroy",  :controller=>"cars"}

といったURIが作られます。

名前つきrouteは、モデルのインスタンスを指定してあげるだけ。

@user = User.find(1)
user_url(@user)  #=> http://localhost:3000/users/1
user_path(@user) #=> /users/1

@car = @user.find(2)
user_car_path(@user, @car)  #=> /users/1/cars/2

さらにすごいのは、link_toとかredirect_toとかform_for

@user = User.find(1)
link_to(h(@user.name), @user)  #=> link_to h(@user.name), :controller => "users", :action => "show", :id => 1
redirect_to(@user) #=> redirect_to "/users/1"
form_for(@user)  #=> action="/users/1" method="PUT"

@user = User.new
redirect_to(@user) #=> redirect_to "/users"
form_for(@user)  #=> action="/users" method="POST"

オブジェクトの状態を見て、空のときとそうでないときとで、URIが変化するのだ。これはすごい。
 ちなみに、carsのようなhas_many状態のrouteでは、単純にオブジエクトを指定するのではなくて名前つきroutesを使用したほうがよさそう。

render の collection は遅かった

 viewの中で、render :partial を使うことはよくあるけど、コレクションのデータを表示するときにぼくは今までcollectionを使ってました。

<%= render :partial => "item", :collection => @items %>

 partialの先でループで回すやり方もありますけど、

<%= render partial => "item_list", :object => @items %>

どちらが速いのか調べてみました。

Rendered _item (0.00498)
Rendered _item (0.00248)
Rendered _item (0.00150)
Rendered _item (0.00149)
Rendered _item (0.00148)
Rendered _item_list (0.00158)

というわけで、partial先でループで回したほうが圧倒的に速かったです。

Rails2.0PRでRspecを走らせる

 朝のエラーの原因は、JavaScriptMacrosHelperがRails2.0からpluginにキックアウトされるようになったため。Railsコアはできるだけシンプルにするつもりみたい。というわけで、単純だけどRspecの該当モジュールを呼ぶところでコメントアウトしてみた。
 ……んだけどまだうまく動かない。どうも、Rspecもedge版を使わないといけないようだ。なので、edge版を入れてみる。

ruby script/plugin install svn://rubyforge.org/var/svn/rspec/trunk/rspec
ruby script/plugin install svn://rubyforge.org/var/svn/rspec/trunk/rspec_on_rails 

 script/generate rspec を忘れずに。これで動きました。

Rspecでコントローラのspecファイルを書く

 Rspecを超いまさらながらやっています。Rspecで一番よくわからないのがMock
とstubで、いまでも両者の違いがよくわかっていないし、mockはあるオブジェクトのフリをする仮想的なオブジェクトで、stubはあるインターフェースを偽装するボックスなのかなあとも考えたりしていますが、はっきりと理解していません。
 今、趣味でとあるwebアプリを作っていて、そのログイン処理の部分の仕様をRspecで書いてみました。

  before(:each) do
    @user = mock("user")
  end

まずはUserモデルのインスタンスのフリをするmockオブジェクトをbeforeで作っておきます。Rspecの例などを見てみると、そのmockオブジェクトやPersonクラスにとstubを定義したりしていますが、今回のログイン仕様ではUserモデルのインスタンスは作らないので用意していません。
 で、ログインが成功する部分の仕様を書きます。

  it "should log in" do
    User.should_receive(:authenticate).with("abe@mail.com", "abe_pass").and_return(@user)
    post "login", {:email => "abe@mail.com", :password => "abe_pass"}
    response.should be_redirect
    response.should redirect_to(index_url)
    assigns[:current_user].should == @user
  end

 まずはUserモデルにstubを定義します。それぞれのitブロックの中で、それに関係するstubを宣言するのが、Rspecのやりかたみたいです。before部では全体に関係するようなstubだけを定義するらしい。しかも、そのときはshould_receiveメソッドではなくstub!メソッドを使って、検証はさせないようにするようです(こんな理解でいいのかな?)。
 これで、正当なメールアドレスとパスワードをPOSTメソッドで送ると、indexへリダイレクトされてログイン状態になるという仕様が定義されました。あとはこれを通るようにコードを書くだけです。
 ログインに失敗する部分の仕様は下記のとおり。

  it "shouldn't log in" do
    User.should_receive(:authenticate).with("itou@mail.com", "invalid_pass").and_return(nil)
    post "login", {:email => "itou@mail.com", :password => "invalid_pass"}
    response.should be_success
    response.should render_template("account/login")
    assigns[:current_user].should be_nil
  end

Rspecではそれぞれの検証が分離している

 言葉にすると至極あたりまえの話のような気もしますが、Rspecでは、モデルとコントローラとビューのテストはそれぞれ分離されています(Rspecでも、should have_tagでビューの検証ができてしまいますが、結合度が密になりやすそうなのであまり使わないほうがいいかもです)。結合度を疎にすることによって、コントローラの仕様を考えるときはそれに専念することができます。そして、どこかのエラーが他に影響を及ぼすのを防いでいます。例えば、ビューの仕様を変更してエラーが出たとき、そのビューの仕様をコントローラに書いているとコントローラでもエラーが出る(当たり前すぎることを書いていますね)のです。
 とはいえ、Test::Unitのfunctionテストとかだと、結構普通にコントローラとビューのテストを一緒に書いちゃってます。

  def test_diary
    get "diary", {:id => diaries(:abe_1).id}
    assert_response :success
    assert_template "diary/show"
    assert_select "#diary_#{diaries(:abe_1).id}" do
      assert_select "#title", diaries(:abe_1).title
      assert_select "#body", diaries(:abe_1).body
    end
  end

 ここでデザイナさんが、日記の内容のIDを"#message"に変えたりすると、このテストはエラーになってしまいます。しかし、コントローラがおかしくてエラーになるわけではないのです。
 さらにいうと、ここでDiaryモデルがid値からインスタンスを取得する部分の仕様を変えたときに、エラーになる可能性があります。
 ここで疎結合を保つために登場するのがmockとstubなのですね。なるほど。

alias_method_chainを使ってみる

 会社の同僚の方に一度教えてもらったのですが、うろ覚えだったので復習しておきます。
alias_method_chainは、既存のメソッドを継承クラスなんて作らなくてもオーバーライドしてくれます。メソッドの再定義と違うのは、オーバーライド前のメソッドも呼べてしまうところ。
 まずは、メソッドを定義します。

class Integer
  def next_with_goodby
    next_without_goodby.to_s + ", and goodby"
  end
  alias_method_chain :next, :goodby
end

こうしておくと、

3.next #=> "4, and goodby"

となります。ポイントはnext_without_goodbyというメソッド。これがオーバーライド前のメソッドになります。

バーベキューがおもしろかった

 土曜日は葛西臨海公園で、みんなとバーベキューにいきました。寒かったけど、豚汁とワインであったまったら寒さは感じなくなります。途中で記憶がなくなったため、最後がどうなったのかよく覚えていません。とても楽しかったことだけは覚えているのだけど。ステーキや焼きそばも食べたらしいのですが、全然覚えていません(もったいないことをした)。どうもユルさんの話によると、みんなにご迷惑をおかけしたようです。いけませんいけません。もう大人なんだから自重しなくては。帰りはユルさんに嘘のルートを教えたらしく、気がついたら新松戸にいました。すげえ。

 今日は二日酔いと謎の筋肉痛で、ずっと家にいました。ニコニコとプログラミングだけで一日が終わってしまった。

アルゴリズム超重要

 はてなダイアリーには、あるキーワードに対して自動的にリンクがはられる機能があります(ユルさんはこの機能が嫌いなのだそうです)。キーワードがはてなでは20万以上も存在しているので、ブログの記事の中にその20万のキーワードのどれかが存在しているかどうかを調べるなんてすごく大変そう(というか時間がすごくかかりそう)な印象を受けます。
 で、はてなではキーワード検索用の正規表現を公開していたりします。ためしに6000文字程度の記事に検索をかけてみます。

#!/usr/bin/perl
use strict;
use warnings;
use IO::File;
use Time::HiRes;

my $k_f = IO::File->new("hatena_keywordlist", "r") or die $!;
my $keyword = $k_f->getline;
$k_f->close;

my $fh = IO::File->new("blog.txt", "r") or die $!;
my @f_lines = $fh->getlines;
$fh->close;

my $str = "";
$str .= $_ for (@f_lines);

my @list;
my $s_time = Time::HiRes::time();
$str =~ s|
    ($keyword)
|
    push @list, $1;
|egiox;
my $e_time = Time::HiRes::time();

print "$_ " for @list;
print "\n";
print "time: ", $e_time - $s_time, "\n";

ひさしぶりに稚拙なPerlです(はてなの正規表現がRubyで使えなかったので)。で、計測結果は、

time: 0.810735940933228

でした。
正規表現は非決定性有限オートマトンを使っているそうなので、DFAで検索するようにすれば速くなりそうです。
で、Rubyで作ってみました。仕事で使うことになりそうなので、とりあえずソースの公開はやめておきます。別にすごいことをやっているわけじゃなくて、ただの決定性オートマトンだけど。20万件のキーワード、6000字ほどの記事という、Perlと同条件でやってみた計測結果は

time: 0.010049

となりました。めちゃめちゃ速くなって一安心。
たぶん、はてなも正規表現は使ってないんじゃないかなあ、という気もします。

簡単サーバー監視ツールを書いてみた

 サーバーの生き死にを監視するツールをRubyで書いてみました。いちよ、L3層とL7層の監視ができます。

#!/usr/local/bin/ruby
require 'ping'
require 'net/smtp'
require 'net/http'

unless Ping.pingecho("www.server.com", 3, "80")
  Net::SMTP.start('mail.server.com', 25) {|s| s.send_mail "Subject:Server Down!!(L3)", 'kanshi-24@mail.server.com', 'oishi@example.com'}
end

begin
  Net::HTTP.start("www.server.com")
rescue
  Net::SMTP.start('mail.server.com', 25) {|s| s.send_mail "Subject:Server Down!!(L7)", 'kanshi-24@mail.server.com', 'oishi@example.com'}
end

サーバーが壊れた

サーバーが壊れました。
この連休でサーバーのOSをCentOSからDebianに移行しようとしたのですが、CD-Rの読み込みがおかしくなり、電源も入らなくなってしまいました。どうも復活は無理のようです。kaeruspoonmilookのデータはバックアップを取っておいたので、復活はできるのですが、ちょっと今はやる気がない状態。

というわけで、しばらく日記は、開発環境上で更新することになりそうです。誰かがここをいつか見ることがあるのでしょうか…。まいいや。nowaをつかって日記を書いていこうかとも思ったのだけど、どうも使う気がせず。すごくいいブログだとは思うんだけど。