現場で役立つRuby on Railsパターン(RubyKaigi2009)を聴いたまとめ

今年はRubyKaigi に行ってきました。今回聴いたセッションの内、いくつかのメモを残します。全てを1エントリーにまとめるのは量的にも厳しいし、エントリの切り方としても微妙なので小分けにして書いていきます。
あくまでメモなので間違いがあるかもしれません。特にコードについては詳細はメモしていなかったので必要な部分は自分で補完したりしました。
というわけで、最初は「株式会社万葉 大場さん」のセッションです。

Pragmatic Patterns of Ruby on Rails - 現場で役立つRuby on Railsパターン(大場 さん)

学生時代はAPI 仕様書を読むのが好きだった大場さんは、周りに同じような趣味を持つ人がいなくて孤独だった。Ruby コミュニティの人達に出会い感動し、自分も何かしたいと思ったと最初におっしゃっていました。

概要

今回のセッションの要点は次のようなことでした。

  • プロジェクト内では次のような問題が発生し、アプリケーションのメンテナンスがし辛くなる
    • コードの書き方がばらばら
    • 品質が揃っていない
  • コードを良い状態に保つ必要がある
    • 良い設計
    • 読みやすいコード
  • 良い状態であれば、複雑なロジック部分だとしてもどんどん変更していくこともできる
  • その為に、チーム内で共通パターンを作成する
    • こういう場合にはRails 的にはこう言う書き方が良いんじゃないかというカタログ
    • DSL を作ったりもする(但し控えめに)
    • これを「実装パターン」とおっしゃっていた

実装パターン

  • 今回は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 が壊れている
    • 実際この例を見たことがあるらしい…
  1. パラメータによる条件分岐をモデルに移行
    • モデルに属性として定義してしまう
    • カラムではなく属性ね
  2. コントローラ内でモデルを操作しているメソッドをモデルに移行
    • コールバックを使う
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 の設計から

質疑応答

  • モデルの肥大化にどう対応すればいいか
    • アスペクト思考的に機能毎にモジュールに切り出す
    • 名前空間は「ModelName::」みたいに切って衝突を避ける

感想

パターン化してチームで共有するというプラクティスは自分のチームにも取り入れたいと感じました。自分は朝来たら前日の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 を使ったキーワード引数。名前はもちろんoptions
  • each のブロック内でアソシエーションメソッドを呼び出す
  • render :partial しまくり。なんかやたら:locals ハッシュの値が多いことに…


強力な道具を手にしたから自分も強くなったと錯覚して力を誤用するという典型的なパターンです…。Rails も強力なフレームワークなので、使う側としては責任を持たないといけないと改めて感じました。
良いセッションでした。大場さんありがとうございました!