はじめに
- 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_name
とlast_name
をまとめたfull_name
というメソッドはデコレーター案件です。
# 表示に関する処理
def full_name
"#{last_name} #{first_name}"
end
- そのデコレーターを実現する方法として、
draper
(ドレイパー?ドラパー?某Railsコミッターさんはドレイパーと仰っていたが‥‥)とactive_decorator
とがあります。 - 今回はこの二つを深く比較していこうと思います。
Draper
- https://github.com/drapergem/draper
- draperコードリーディング
- まずは私が長らくお世話になっている
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
を使って、ActiveModel
にinclude 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モデルをつけなくてもデコレーターメソッドを使える。
- というのも、初期化時に
ActiveModel
やActionController
に対して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
終わりに
- 解体して見ると、設計思想って色々あるんだなぁと思った