LoginSignup
7
9

More than 5 years have passed since last update.

RailsのデコレーターGem、draperとactive_decoratorを比較してみた

Posted at

はじめに

  • Railsにおけるデコレーターとはそもそも何か。

Imagine your application has an Article model. With Draper, you'd create a corresponding ArticleDecorator. The decorator wraps the model, and deals only with presentational concerns. In the controller, you decorate the article before handing it off to the view
訳)あなたのアプリにArticleモデルがあったとします。Draperを使うと、そのモデルに対応するArticleDecoratorを作成することになります。デコレーターはモデルをラップして、表示に関する処理のみを管理します。なおコントローラー内で使うときは、ビューに表示する前にモデルをデコレーションする必要があります。

  • つまり表示に関する処理を切り出して、モデルの肥大化を防ぐ為にデコレーターを作るということです。
  • わかりやすい例えで言うと、Userモデルのfirst_namelast_nameをまとめたfull_nameというメソッドはデコレーター案件です。
# 表示に関する処理
def full_name
  "#{last_name} #{first_name}"
end
  • そのデコレーターを実現する方法として、draper(ドレイパー?ドラパー?某Railsコミッターさんはドレイパーと仰っていたが‥‥)とactive_decoratorとがあります。
  • 今回はこの二つを深く比較していこうと思います。

Draper

generator

  • rails g decorator userと打ったとき、lib/generators/rails/decorator_generator.rbにある通り、テンプレートをクラス名に応じたファイル名で追加する。
  • そのファイル内に登場するdelegate_allは以下の道筋を通って、Decoratorクラス内にメソッドがなければ、元のモデルにフォールバックするようにしている。
# lib/draper/decorator.rb
def self.delegate_all
  include Draper::AutomaticDelegation
end

# lib/draper/automatic_delegation.rb
def method_missing(method, *args, &block)
  return super unless delegatable?(method)

  object.send(method, *args, &block)
end

private def delegatable?(method)
  object.respond_to?(method)
end

decorate

  • Railtieを使って、ActiveModelinclude Draper::Decoratableを実行している。
  • 前提としてデコレーターはDraper::Decoratorの継承であることを覚えておく
  • ActiveRecordからdecorateを呼び出した場合
# lib/draper/decoratable.rb
def decorate(options = {})
  # このselfはUserで、decorator_classはUserDecorator
  decorator_class.decorate(self, options)
end

def decorator_class
  self.class.decorator_class
end

class << self
  def decorator_class(called_on = self)
    # 要約すると、UserモデルならUserDecoratorモデルを返す
  end
end

# lib/draper/decorator.rb
class << self
  def initialize(object, options = {})
    # 元のActiveRecordをobjectとして所持
    @object = object
  end

  alias :decorate :new
end

ActiveDecorator

  • draperと違って、特にdecorateモデルをつけなくてもデコレーターメソッドを使える。
  • というのも、初期化時にActiveModelActionControllerに対してdecorateメソッドを実行しきっているから

decorate

  • decorateメソッドを実行すると、draperとは違って単純にデコレータークラスをextendしてくれる。
class Decorator
  include Singleton

  def initialize
    @@decorators = {}
  end

  # ArrayかActiveRecord::Relation、ActiveRecord::Baseだけdecorateする。
  # それ以外はそのまま返す。
  # decorateされていた場合にもそのまま返す
  # シングルトンクラスのメソッドなので、こうやって呼び出す。
  # `ActiveDecorator::Decorator.instance.decorate(obj)`.
  def decorate(obj)
    # Jbuilderとnilを弾く
    return if defined?(Jbuilder) && (Jbuilder === obj)
    return if obj.nil?

    if obj.is_a?(Array)
      obj.each do |r|
        decorate r
      end
    elsif defined?(ActiveRecord) && obj.is_a?(ActiveRecord::Relation)
      # recordsメソッドを呼んだ時に、自動的にdecorateされた物を返すようにする
      if obj.respond_to?(:records)
        # Rails 5.0
        obj.extend ActiveDecorator::RelationDecorator unless obj.is_a? ActiveDecorator::RelationDecorator
      # Rails 5.x以前はrecordsメソッドがなかったんか‥‥
      else
        # Rails 3.x and 4.x
        obj.extend ActiveDecorator::RelationDecoratorLegacy unless obj.is_a? ActiveDecorator::RelationDecoratorLegacy
      end
    else
      # ActiveDecorator::Decoratedがextendされているか確認
      # ActiveDecorator::Decorated自体は空のモジュールであり、そのクラスがデコレートされたかどうかのフラグの役割を果たしている。
      if defined?(ActiveRecord) && obj.is_a?(ActiveRecord::Base) && !obj.is_a?(ActiveDecorator::Decorated)
        obj.extend ActiveDecorator::Decorated
      end

      # 最後に、objectがまだdecorateされてなければ(ModelnameDecorateがextendされてなければ)extendする
      d = decorator_for obj.class
      return obj unless d
      obj.extend d unless obj.is_a? d
    end

    obj
  end

  private
  # Returns a decorator module for the given class.
  # Returns `nil` if no decorator module was found.
  def decorator_for(model_class)
    return @@decorators[model_class] if @@decorators.key? model_class

    decorator_name = "#{model_class.name}#{ActiveDecorator.config.decorator_suffix}"
    d = decorator_name.constantize
    unless Class === d
      d.send :include, ActiveDecorator::Helpers
      @@decorators[model_class] = d
    else
      # Cache nil results
      @@decorators[model_class] = nil
    end
  rescue NameError
    if model_class.respond_to?(:base_class) && (model_class.base_class != model_class)
      @@decorators[model_class] = decorator_for model_class.base_class
    else
      # Cache nil results
      @@decorators[model_class] = nil
    end
  end
end

# For AR 3 and 4
module RelationDecoratorLegacy
  def to_a
    super.tap do |arr|
      ActiveDecorator::Decorator.instance.decorate arr
    end
  end
end

# For AR 5+
module RelationDecorator
  def records
    super.tap do |arr|
      ActiveDecorator::Decorator.instance.decorate arr
    end
  end
end

終わりに

  • 解体して見ると、設計思想って色々あるんだなぁと思った
7
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
9