初めまして、19年新卒webエンジニアの平井蒼大です。
弊社では、昼休憩時間を使って、最近勉強したこと、 興味があること、最近行った勉強会やカンファレンスの内容などをLT形式で自由に発表するHacker’s Delightという場が設けられています。
私も先日、「Draperのソースコードリーディング」というお題で発表しましたので、その内容を掲載したいと思います。
動機
今回、Draperのソースコードリーディングに至った理由は以下の二つです。
- Draperの仕組みを知りたい。
- Ruby, Ruby on Railsについての知識を増やす。
Draperとは
DraperはPresenter層を提供するgemです。
draperを使うことで以下の利点があります。
- モデルの肥大化を防ぐ
- グローバル空間ににヘルパーメソッドが追加されることを防ぐ。
理解する部分
今回、ソースコードリーディングを通して理解するDraperの機能は以下の二つにしました。
また、今回、ArticleクラスをラップしたArticleDecoratorクラスを生成してDraperを使った状況を下に、説明したいと思います。
Decoratorクラスのインスタンス生成方法
下のコードのように、ラップされたクラス(Articleクラス)のインスタンスに.decorateすることで、Decoratorクラスのインスタンスが生成されます。この動きをソースコードを読んで理解しました。
Article.first.decorate
ラップされたクラスのメソッドの呼び出し
Decoratorクラス内でdelegate_allを呼び出すと、ラップされたクラスのメソッドを使うことができます。下の例では、delegate_allすることで、Articleクラスで定義されているpublished?メソッドを呼び出しています。
この動きもソースコードを読んで理解しました。
class ArticleDecorator < Draper::Decorator delegate_all def publication_status if published? "Published at #{published_at}" end end end
Decoratorクラスのインスタンス生成方法
Decoratorクラスのインスタンスが生成されるまでの流れを以下の二つのセクションに分けて、説明したいと思います。
- Railtieによって、Draper::DecoratableモジュールがActiveRecord::Baseにincludeされる流れ
- decorateメソッドの中身
つまり .decorateが呼ばれる前のDraperが読み込まれた際の処理と、.decorateが呼ばれた後の流れを分けて説明します。
Railtieによって、Draper::DecoratableモジュールがActiveRecord::Baseにincludeされる流れ
まずは、railtie.rbを読んで、どのような初期化ステップが設定されているかを見ます。
今回関係するソースコードは以下になります。
module Draper class Railtie < Rails::Railtie # 他の初期化ステップが追加されている initializer 'draper.setup_orm' do [:active_record, :mongoid].each do |orm| ActiveSupport.on_load orm do Draper.setup_orm self end end end
Draper::Railtieクラスは、Rails:Railtieクラスのサブクラスなので、Railsのブートプロセス時に呼ばれます。
initializerの働きは以下です。
- ブロックの内容をdraper.setup_ormという名前の初期化ステップとして設定
- 初期化ステップ自体はMyapp.application.initialize!が呼ばれた時に呼び出される
次に、initializerのブロック内の説明をします。
ActiveSupport.on_loadは activesupport/lib/active_support/lazy_load_hooks.rbで定義されています。
def on_load(name, options = {}, &block) @loaded[name].each do |base| execute_hook(name, base, options, block) end @load_hooks[name] << [block, options] end def run_load_hooks(name, base = Object) @loaded[name] << base @load_hooks[name].each do |hook, options| execute_hook(name, base, options, hook) end end private def with_execution_control(name, block, once) unless @run_once[name].include?(block) @run_once[name] << block if once yield end end def execute_hook(name, base, options, block) with_execution_control(name, block, options[:run_once]) do if options[:yield] block.call(base) else if base.is_a?(Module) base.class_eval(&block) else base.instance_eval(&block) end end end end
今回の場合だとon_loadは、@loaded(:active_record)に対応するものがあれば、そのブロックを即時実行し、なければ@load_hooksに :active_recordというキーでDraper.setup_orm selfを格納します。
この@load_hooksに格納されたDraper.setup_orm selfはlazy_load_hooks.rb内で定義されているrun_load_hooksが呼ばれた時に実行されます。
run_load_hooksはActiveRecord::Base内で、以下のように呼ばれています。
ActiveSupport.run_load_hooks(:active_record, Base)
今回の場合、run_load_hooksメソッド内のbaseはActiveRecord::Baseです。@load_hooks[:active_record]にはload(:active_record) { Draper.setup_orm self } によって Draper.setup_orm selfが格納されています。
つまり、ActiveRecord::Baseで、run_load_hooks(:active_record, Base)が呼ばれると、lazy_load_hooks.rb 内のexecute_hook内でActiveRecord::Base.instance_eval{ Draper.setup_orm self } が実行されます。
Draper.setup_ormはDraperモジュールで定義されています。
def self.setup_orm(base) base.class_eval do include Draper::Decoratable end end
instance_eval内のselfはレシーバーのオブジェクトなのでDraper.setup_orm self のselfはActiveRecord::Baseです。
つまり、Draper.setup_orm(ActiveRecord:Base)が呼ばれ、ActiveRecord::BaseがDraper::Decoratableをincludeします。
decorateメソッドの中身
先程説明した流れで、Draper::DecoratableがActiveRecord::Baseにincludeされます。
Article.first.decorate
のdecorateメソッドはDraper::Decoratable内で定義されているdecorateメソッドに対応します。
Draper::Decoratableは以下のようになっています。
module Draper module Decoratable extend ActiveSupport::Concern def decorate(options = {}) decorator_class.decorate(self, options) end def decorator_class self.class.decorator_class end module ClassMethods 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
decorateメソッド内では直下のdecorator_classメソッドを呼びます。decorator_class内のselfはArticleのインスタンス(Article.first)です。
extend ActiveSupport::ConcernがあるのでClassMethods配下のメソッドはそのモジュールがincludeされたクラスのクラスメソッドになります。ですので、上のdecoratorメソッド直下のdecoratar_class内では、Decoratable::ClassMethods内のdecorator_classを呼び出しています。
このdecorator_classで、ラップされたクラス(Articleクラス)のDecoratorクラス(ArticleDecorator)を取得しています。
このdecorator_class内では、Articleのインスタンスがmodel_nameを持つか調べ、あったらprefixとします。そのprefixとDecoratorを文字列連結した変数とsafe_constantizeでDecoratorクラスを取得しています。
つまり、Decoratable直下のdecorateメソッド内のdecorator_classはArticleDecoratorになり、メソッド内ではArticleDecorator.decorate(self, options)を実行しています。
この時の、decorateは ArticleDecoratorクラスが継承しているDraper::Decoratorクラスのinitializeに対応しています。
module Draper class Decorator def initialize(object, options = {}) options.assert_valid_keys(:context) @object = object @context = options.fetch(:context, {}) handle_multiple_decoration(options) if object.instance_of?(self.class) end class << self alias_method :decorate, :new end
このような流れで、ArticleDecoratorクラスのインスタンスが生成されます。
ラップされたクラスのメソッドの呼び出し
Decoratorクラス内でdelegate_allを呼び出すと、ラップされたクラスのメソッドを使うことができる仕組みをソースコードを読んで理解したいと思います。
delegate_allメソッドはDraper::Decoratorクラスで定義されています。
module Draper class Decorator def self.delegate_all include Draper::AutomaticDelegation end
AutomaticDelegationモジュールをincludeしています。
AutomaticDelegationではmethod_missingが定義されています。
module Draper module AutomaticDelegation extend ActiveSupport::Concern def method_missing(method, *args, &block) return super unless delegatable?(method) object.send(method, *args, &block) end def delegatable?(method) return if private_methods.include?(method) object.respond_to?(method) end # 他のメソッド
method_missingの中で使用されているobjectは、ArticleDecoratorクラスが継承しているDraper::Decoratorクラスのinitializeの中で定義されています。
def initialize(object, options = {}) options.assert_valid_keys(:context) @object = object @context = options.fetch(:context, {}) handle_multiple_decoration(options) if object.instance_of?(self.class) end
このinitializeはDraper::Decoratableモジュール(ActiveRecord::Baseにincludeされた)のdecorateメソッド内で、.decorate(self, options)という形で呼ばれています。
module Draper module Decoratable extend ActiveSupport::Concern def decorate(options = {}) decorator_class.decorate(self, options) end
この第一引数がobjectに当たるので、今回の場合のmethod_missing内のobjectはActicleクラスのインスタンスになります。
method_missingのソースコードに戻ります。
module Draper module AutomaticDelegation extend ActiveSupport::Concern def method_missing(method, *args, &block) return super unless delegatable?(method) object.send(method, *args, &block) end def delegatable?(method) return if private_methods.include?(method) object.respond_to?(method) end # 他のメソッド
method_missingは呼び出さそうとしたメソッドが定義されていない時に、呼び出されます。引数のmethodには呼び出そうとしたメソッド名がシンボルで入ります。delegatable?では、プライベートメソッドを呼び出そうとしているか、また、Articleクラスのインスタンスが引数methodに入れたメソッドを呼び出せるかを調べています。
よって、method_missingの1行目では、呼び出そうとしているメソッドがプライベートメソッドまたは、Articleクラスのインスタンスメソッドでない場合、祖先クラス(objectクラス) のmethod_missingが呼ばれ、NoMethodErrorを発生させます。
そうではない場合、object(Articleクラスのインスタンス)にメソッドを渡して、メソッドを実行しています。このような仕組みでdelegate_allをするとラップされたクラスのメソッドを使うことができます。
所感
最初にも、記載させていただきましたが、今回ソースコードリーディングに至った理由としては以下の二つが挙げられます。
- Draperの仕組みを知りたい
- Ruby, Ruby on Railsについての知識を増やしたい
最後にこれらの理由に沿って、今回のソースコードリーディングの感想を書きたいと思います。
Draperの仕組みを知りたい
Draper全体の機能と比べるとかなり一部ではありますが、仕組みをソースコードベースで理解することができました。ソースコードリーディング前の仕組みを理解できていないモヤモヤ感が晴れた瞬間がソースコードリーディングの楽しみの一つなのかなと感じました。
Ruby, Ruby on Railsについての知識を増やしたい
railtieで、Railsの初期化処理を拡張したり、method_missingの使い方、その他のメソッドなど今回のソースコードリーディングで知識を増やすことができたと思います。ただ、railtieによって初期化ステップが設定される流れ、その初期化ステップが実際に実行される部分などは、さらに調べて理解度をあげたいと感じました。
また、 発表という形をとったため、Draperの仕組みについて言葉で伝えられる程理解する必要がありました。準備は大変でしたが、理解度をあげることができたのはとても良かったと感じています。
最後まで読んでいただきありがとうございます。