コードリーディング:DraperのDecorator探索ロジックを追う
Draper が、具体的にどういったロジックでDecoratorクラスを探索するかを調べた。
バージョンは v4.0.1
。
想定する状況
次のように、#decorate
メソッドをActiveRecordのオブジェクトに対して呼び出す状況を想定する。
User.last.decorate
=> #<UserDecorator:0x00007fd0eae2ec18 ..>
ほとんどの場合、User
モデルがあるならば UserDecorator
を用意しておくことになるわけだが、Draperが内部的にこれをどうやって探索しているかというところを今回はコードレベルで追ってみたい。
最初に結論
#decorate
メソッドのレシーバとなるオブジェクトのクラス名に "Decorator" というsuffixをつけたクラスがデコレータクラスになる- もしそのデコレータクラスが存在しない場合は、
#decorator_class
メソッドに応答する限り親クラスを再帰的に探索していく
コードリーディング
Draper::Decoratable#decorate
まずはエントリポイントとなる #decorate
メソッドを探す。
単にgrepすると同名のメソッドが複数見つかるが、lib/draper/decoratable.rb
のメソッドが探している方。
# lib/draper/decoratable.rb
def decorate(options = {})
decorator_class.decorate(self, options)
end
内部では #decorator_class
というメソッドが呼ばれており、その返り値に対して #decorate
を呼んでいることがわかった。
早速ではあるが、今回の調査目的である「デコレータクラスを見つけ出す」処理に関連がありそうな名前のメソッドである。
Draper::Decoratable#decorator_class
ということで今度は #decorator_class
メソッドを探す。これは同ファイル内のすぐ真下にある。
# lib/draper/decoratable.rb
def decorator_class
self.class.decorator_class
end
self.class
はライブラリの文脈で考えると、Draper::Decoratable
を指す。なので、Draper::Decoratable
にある特異メソッド .decorator_class
を探せば良い。
Draper::Decoratable::ClassMethods#decorator_class
引き続きこのファイル内を探すと ClassMethods
というモジュールの中に該当のメソッドが見つかる。これはActiveSupport::Concernの機能になるが、includeした側にクラスメソッドとして取り込む機能となる。ぱっと見た感じ、ここに今回の目的となる探索ロジックがありそうだ。ここは注意深く読んでいく。
# lib/draper/decoratable.rb
def decorator_class(called_on = self)
prefix = respond_to?(:model_name) ? model_name : name
decorator_name = "#{prefix}Decorator"
decorator_name_constant = decorator_name.safe_constantize
return decorator_name_constant unless decorator_name_constant.nil?
if superclass.respond_to?(:decorator_class)
superclass.decorator_class(called_on)
else
raise Draper::UninferrableDecoratorError.new(called_on)
end
end
まず1行目。
prefix = respond_to?(:model_name) ? model_name : name
model_name
はActiveModel::Naming#model_name
である。返り値は文字列ではなくActiveModel::Name
オブジェクトであるが、to_s
をオーバライドしているので、puts
に与えたり文字列展開をすると文字列でモデル名を取得できる。name
はRubyコアのModule#name
である。これは単にクラス・モジュール名を文字列で返す。
今回の想定ケースでは #model_name
を使う方を頭に入れて次に進む。
もしUserモデルであれば、prefix
変数には "User"
文字列が代入されることになる。
その次の2行。
decorator_name = "#{prefix}Decorator"
decorator_name_constant = decorator_name.safe_constantize
上の行で取得したprefixに "Decorator"
という文字列をくっつけて変数に代入している。
その次の #safe_constantize
はActiveSupportがStringクラスを拡張して生やしたメソッドで、レシーバに対応するクラス(定数)を探索して見つかった場合はそのクラスを返し、見つからない場合はnilを返すという動きをする。
https://api.rubyonrails.org/classes/String.html#method-i-safe_constantize
つまり、Userモデルであれば "UserDecorator"
という文字列が decorator_name
変数に格納され、その次の行で UserDecorator
クラスが存在すればそれを、もし存在しなければnilが decorator_name_constant
変数に代入されることになる。
その次の1行。
return decorator_name_constant unless decorator_name_constant.nil?
decorator_name_constant
がnilじゃない、すなわち対応するDecoratorクラスが見つかった場合は、returnでそのクラスを返してメソッドは終了する。
今回想定しているケースではここでreturnされるが、残り数行なので見つからなかった場合もついでに読んでみよう。それが次の5行となる。
if superclass.respond_to?(:decorator_class)
superclass.decorator_class(called_on)
else
raise Draper::UninferrableDecoratorError.new(called_on)
end
decorator_class
メソッドの呼び出しに応答する限り、スーパークラスを再帰処理で辿っていくようだ。つまりもしUserモデルがBaseモデルを継承しているとして、UserDecoratorを用意していなくともBaseDecoratorがあればそれが使われるということがわかる。
decorator_class
メソッドに応答できなくなった段階でデコレータクラス推測不可の例外が上がるようだ。
感想
普段から使っているライブラリで当然のように知っている仕様に対して内部のコードを読んでみた。振る舞いを理解できているので、コードを読むときの推測もおおよそ当たっていた気がする。
それでも親クラスを辿る仕様だったり、 #safe_constantize
のようなメソッドだったり、ActiveSupport::Concern
の機能だったりをコードを読む過程で学ぶことができたのが良かった。