現場で役立つRuby on Railsパターン(RubyKaigi2009)を聴いたまとめ
今年はRubyKaigi に行ってきました。今回聴いたセッションの内、いくつかのメモを残します。全てを1エントリーにまとめるのは量的にも厳しいし、エントリの切り方としても微妙なので小分けにして書いていきます。
あくまでメモなので間違いがあるかもしれません。特にコードについては詳細はメモしていなかったので必要な部分は自分で補完したりしました。
というわけで、最初は「株式会社万葉 大場さん」のセッションです。
Pragmatic Patterns of Ruby on Rails - 現場で役立つRuby on Railsパターン(大場 さん)
学生時代はAPI 仕様書を読むのが好きだった大場さんは、周りに同じような趣味を持つ人がいなくて孤独だった。Ruby コミュニティの人達に出会い感動し、自分も何かしたいと思ったと最初におっしゃっていました。
実装パターン
- 今回はActiveRecord(以下AR) を中心として話す
- やっぱりAR はいい
- OOP の原則に従って書ける
- 複雑さに耐えられるようにできている
- 今回話さないこと
- パフォーマンスについて
パターン1: 権限のあるデータ
概要
- ブログシステムを考えた時、「ユーザが見れるのはユーザ自身が書いたブログのみ」というような場合にどうするか
良くない例
- 動的Finder
@note = Note.find_by_id_and_user_id(params[:id], current_user.id)
- NamedScope
@note = Note.written_by(current_user.id).find(params[:id])
- with_scope
- この例は自分での補足
- 今のAR だとできないけど、古いRails で以下のように書いたことがある
Note.with_scope(:find => {:conditions => "user_id = #{current_user.id}"}) do @note = Note.find(params[:id]) end
何故良くない
- ユーザid で絞らなきゃいけないのを忘れてしまう可能性がある
- 忘れて「Note.find(params[:id])」ってやってもそのまま動いてしまう
- 書き間違いが見つけにくくなる
どうすればいいのか
current_user.notes.find(params[:id])
- オブジェクトから始めるように書くのがポイント
- アクセス権利者を明確に意識できるので忘れない
パターン2: モデルオブジェクトをフィルタ経由で取得する
概要
- タイトルにある通り
良くない例
def show @group = Group.find(params[:group_id]) @notes = @group.notes.find(params[:id]) end def new @group = Group.find(params[:group_id]) @notes = @group.notes.build end
何故良くない
- DRY じゃない
どうすればいいのか
class GroupNotesController < ApplicationController before_filter :find_group private def find_group @group = Group.find(params[:group_id]) end end
- DRY になる
- 例えば、自分が所属するグループのノートしか操作できないようアクセス制限するならfind_group だけ変更すればいい
def find_group @group = current_user.groups.find(params[:group_id]) end
- コントローラ名とフィルタ宣言を見れば何やっているのか大体分かるようになって読みやすい
- @group というリソースを起点にして何かするようアクションを強制できる
- Group に関係ないNote の操作をしちゃったりみたいな変な設計に行くのを防止できる
パターン3: コントローラ内での複雑なロジック
概要
- Controller 内にprivate メソッドを生やしてそこでごにょごにょやっちゃうような場合、それをモデルロジックとして移行するには
- 例として、Note を保存すると、その本文からタグを自動生成する機能を考える
- このような付随する他モデルへの処理はコントローラに書いてしまいがち
良くない例
def create @note = Note.new(params[:note]) if params[:auto_tagging] generate_tags(@note) end end private def generate_tags(note) ... end
何故良くない
- コントローラ内に余計なメソッドがある
- パラメータによる条件分岐によりアクションのロジックが複雑になる
どうすれば良いのか
- 基本的にはモデルに集める
- でも以下のような集め方は問題
class Notes < ActiveRecord::Base def do_create(params) ... end end
- MVC が壊れている
- 実際この例を見たことがあるらしい…
- パラメータによる条件分岐をモデルに移行
- モデルに属性として定義してしまう
- カラムではなく属性ね
- コントローラ内でモデルを操作しているメソッドをモデルに移行
- コールバックを使う
class Notes < ActiveRecord::Base attr_accessor :auto_tagging def before_save if auto_tagging? genarate_tags end end private def genarate_tags ... end def auto_tagging? !!auto_tagging end end
- どういう条件に対して何をするか、モデルだけを見れば分かるようになる
- これを自立したモデルと表現されていた
どうやってパターンを見い出すのか
- 基本的にOOP を意識する
- それは誰のすべき仕事かにこだわる
- 例えば、パターン1 の例で言えば、「Note.find」のように必ずしもデータのあるテーブルのAR クラスから始める必要はない。User のNote を引くなら「currnet_user.notes.find」の方がOOP 的に自然
- Rails の思想に背かない
- 例えば、Rails にはREST が強く反映されているので、REST から逃げると面倒
- Controller の分け方はREST の原則に従う
- 1コントローラでは1リソースしか扱わない
- /blogs/destroy_comment/1 とかは良くない例
- Controller の設計はURL の設計から
感想
パターン化してチームで共有するというプラクティスは自分のチームにも取り入れたいと感じました。自分は朝来たら前日のTimeline を追って思ったことをIRC にポストするくらいで、ノウハウの蓄積みたいなのはあまりやっていなかったので参考になりました。
個人的にRails 開発をする際に意識していることは、Ruby on Rails Code Quality Checklist抄訳 - moroの日記とか、その層のことはその層のルールに従うことあたりです。後者については、例えばSTI やポリモフィックアソシエーションはDB 原則の「One Fact in One Place」を壊しかねないので控えるとか。
最後に、このセッションを聴いていて、やはりRails から入るとすごい癖がついてしまうと感じました。自分もRails から入ったので、最初はOOP、デザインパターン、DB 設計とか良く分からないままコードを書いていました。その結果次のようなアンチパターンをばしばしやって来たわけです。
- DRY だーとかいってeval 族、define_method 多用
- included + alias_method_chain 多用
- オレオレActiveSupport
- Hash#slice とか、ActiveRecord::Base.find(:last) とか
- Hash を使ったキーワード引数。名前はもちろんoptions
- each のブロック内でアソシエーションメソッドを呼び出す
- render :partial しまくり。なんかやたら:locals ハッシュの値が多いことに…
強力な道具を手にしたから自分も強くなったと錯覚して力を誤用するという典型的なパターンです…。Rails も強力なフレームワークなので、使う側としては責任を持たないといけないと改めて感じました。
良いセッションでした。大場さんありがとうございました!