Draper、混沌としてきたhelperの代替策のひとつ

  • 186
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Rails 4.0.0
ruby 2.0.0p247

Draperとは?

具体例を書いた方が分かりやすいと思うのでコードで。

Draperなし(helper頼み)

controllers/products_controller.rb
class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
  end
end
helpers/products_helper.rb
module ProductsHelper
  def product_name_with_hoge(product)
    "#{product.name} hoge"
  end
end
views/products/show.html.haml
%p=product_name_with_hoge(@product)

みたいな感じになってしまうので、ここだけオブジェクト志向の世界から抜け出た感じになってしまいます。

viewのコードにproductって単語が繰り返されているのもまた気持ち悪いです。
でも、name_with_hogeみたいなメソッド名にしてしまうと別のhelperと競合してしまう可能性があるので仕方ないですね。

Draperあり(helperなし)

controllers/products_controller.rb
class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id]).decorate
  end
end

あらたに#decorateを呼び出しています。

decorators/products_decorator.rb
class ProductDecorator < Draper::Decorator
  def name_with_hoge
    "#{object.name} hoge"
  end
end
views/products/show.html.haml
%p=@product.name_with_hoge

Viewのコードがスッキリになりましたね。
なにより直感的なコードになったと思います。

helperはちょこっと何かをしたいという時は便利だと思うのですが、Viewがゴチャゴチャしがちな気がします。

ほかにできる便利なこと

helperにアクセスできる

decorators/product_decorator.rb
class ProductDecorator < Draper::Decorator
  def name_with_icon
    content = h.content_tag(:i, nil, class: 'icon-user')
    content << object.name
  end
end

#hで既存のhelperにアクセス出来ます。
Railsは便利なhelperがたくさんありますからね。

関連モデルを自動的に#decorateする

decorators/product_decorator.rb
class ProductDecorator < Draper::Decorator
  decorates_association :price
end
decorators/price_decorator.rb
class PriceDecorator < Draper::Decorator
  def created_at_with_hoge
    "#{object.created_at} hoge"
  end
end
views/products/show.html.haml
%p=@product.price.created_at_with_hoge

のようにいちいち#decorateしなくても勝手にdecoratedなmodelがもらえます。

attributeをフィルターする

Viewからアクセスできるattributeを制限したい場合などもたぶん良いです。
アクセスしたいattributeだけを明示的に#delegateします。
(この場合、ふだん#delegateに必要なto:は推測できるので省略できます)

decorators/product_decorator.rb
class ProductDecorator < Draper::Decorator
  delegate :name
end

そんなこと気にしない場合は、

decorators/product_decorator.rb
class ProductDecorator < Draper::Decorator
  delegate_all
end

とすれば、Decoratorに定義されていないメソッドはmodelに渡されます。

共通のメソッドはmoduleで書きだしたり

Rails4(37signales)なスタイルを真似て、decorators/concernsみたいなディレクトリ作って書き出してもいいかもね。

decorators/concerns/timestamps_decoration.rb
module TimestampsDecoration
  def created_at
    object.created_at.strftime('%F %R')
  end
end
config/application.rb
config.eager_load_paths += Dir[Rails.root.join('app', 'decorators', 'concerns')]
decorators/product_decorator.rb
class ProductDecorator < Draper::Decorator
  include TimestampsDecoration
end

でも、TimestampDecorationに対してconcernsってフォルダ名は適切なじゃない気もするな。。

まとめ

modelに属さない便利メソッドはまたhelperとして居続けていいと思うんですよね。

ただ、modelにname_with_hogeみたいなメソッドがあると気持ち悪し、helperに書いてもイマイチなので、decorator(presenter)として書き出すのがいいんじゃないかなと思います。

DraperのほかにもActiveDecorator

ほかに、同様のことが出来るものにamatsudaさんのActiveDecoratorがあります。

その他は、Ruby Tool Box - Rails Presentersにありますが、ActiveDecoratorかDraperを選ぶ人が多そうです。

好みでどちらを使うか選んだらいいかなと思います。

あとがき

ほんとうはconcernsのところだけ書く予定が、Draperの記事がQiitaに見当たらなかったので、まるっと書いてみた次第です。

ほとんどREADME.mdの和訳だが…
詳しくは、drapergem/draperを参照のこと!