Augmented Usamimi

it { is_expected.to be_blog.written_by(izumin5210) }

【Rails】ActiveDecorator読んでみたら超勉強になった

神戸.rb Meetup #10 - Kobe.rb | Doorkeeperで検証してみた内容.


ActiveDecorator?

ActiveDecoratorPresenterなどと呼ばれ, ModelとViewの中間のレイヤーを担う. ViewでModelが関わるロジック等を扱いたいとき(表示を整形したいとか)にHelperの代替として用いられる.

このレイヤーを作るメリットは、

  • model/viewに余計なロジックを書かなくてすむ
  • Helperのメソッドが使われている場所が不明という事態を防ぐ

Draperで驚くほどRailsコードがわかりやすくなったよ! - 酒と泪とRubyとRailsと

Helperを乱用し過ぎるとグローバル関数っぽくてややこしくなったりしますよね. Ruby ToolboxRails Presenterカテゴリでは4位(記事執筆時点). 他にはDraperが有名.

f:id:izumin5210:20150107121321p:plain

Draperとの違いとしては…

  • 明示的にdecorateしなくても良い
  • 関連先Model等はデコられない(partialに投げたらデコってくれる)

どうやって動いてる?

Controllerのインスタンス変数をデコる

AbstractController::Renderingをモンキーパッチ的に拡張することで自動デコり機能を実現してる. Controllerで宣言されたインスタンス変数すべてにdecorateメソッドを適用する. (alias_method_chainなんてメソッド初めて知った.便利だ.)

# lib/active_decorator/monkey/abstract_controller/rendering.rb#L3
def view_assigns_with_decorator
  hash = view_assigns_without_decorator
  hash.values.each do |v|
    ActiveDecorator::Decorator.instance.decorate v
  end
  hash
end

alias_method_chain :view_assigns, :decorator

その変数のクラスにDecoratorが存在した場合に,その変数にDecoratorextendする. また,"#{model_class.name}Decorator"という命名になっているものを探してくる.

  • UserUserDecorator
  • Gochiusa::PyonPyonGochiusa::PyonpyonDecorator
# lib/active_decorator/decorator.rb#L29
d = decorator_for obj.class
return obj unless d
obj.extend d unless obj.is_a? d

Partialの引数をデコる

こちらはActionView::PartialRendererを拡張.localsobjectcollectionをデコってくれる.

# lib/active_decorator/monkey/action_view/partial_renderer.rb#L19 
def setup_with_decorator(context, options, block)
  setup_without_decorator context, options, block
  setup_decorator
end

alias_method_chain :setup, :decorator
# lib/active_decorator/monkey/action_view/partial_renderer.rb#L3
def setup_decorator
  @locals.values.each do |v|
    ActiveDecorator::Decorator.instance.decorate v
  end unless @locals.blank?
  ActiveDecorator::Decorator.instance.decorate @object unless @object.blank?
  ActiveDecorator::Decorator.instance.decorate @collection unless @collection.blank?

  self
end

隠れた特徴

名前に"Active"って入ってるからActiveRecord/ActiveModel限定だと思い込んでいたが,実はそうでもない. デコる対象はインスタンス変数全て,locals全部などクラス問わず,Decoratorの命名にあうものがあればそれを引っ張ってきてくれる.

# app/models/gochiusa.rb
class Gochiusa
  attr_accessor :pyon_pyons
  def initialize
    @pyon_pyons = [Pyonpyon.new]
  end
  class Pyonpyon
  end
end
# app/decorators/gochiusa_decorator.rb
module GochiusaDecorator
end
# app/decorators/gochiusa/pyonpyon_decorator.rb
module Gochiusa::PyonpyonDecorator
end
# app/controllers/gochiusa_controller.rb
class GochiusaController < ApplicationController
  def show
    @gochiusa = Gochiusa.new
  end
end
<%# app/views/gochiusa/show.html.erb %>
<%# @gochiusaはデコられる %>
<%# @gochiusa.pyonpyonsはデコられない %>
<%= render partial: 'gochiusa/pyonpyon', collection: @gochiusa.pyonpyons, as: :pyonpyon %>
<%# app/views/gochiusa/_pyonpyon.html.erb %>
<%# pyonpyonはデコられてる %>

まとめ

  • ActiveDecoratorはなんでもデコってくれる
    • ActiveRecordActiveModel以外でも関係なし
  • 関連先等はpartialに放り込んだらデコってくれる
    • Viewが強制的にSkinnyになる
  • 実装がキレイで読みやすく,勉強になる
    • alias_method_chainの存在はここで初めて知った
    • Railsの内部について知ることができる
      • Controllerやpartialにおける変数の扱い方も学べる

参考