ActiveSupport::Concern#includedを調べた

Railsでよく出てくる includedが分からなかったので調べた時のメモ

まず前提となるinclude, excludeが呼ばれた時の動作は ActiveSupport::Concern のソースコードリーディング #1 復習編 が詳しい。 次に以下の2つの記事を読む。 ActiveSupport::Concern でハッピーなモジュールライフを送る ActiveSupportを読む: ActiveSupport::Concern

これだけで大体理解できるかもしれないが、実際にモジュールをインクルードした時に何が起こるのかを書いておく。

これから先は以下のサンプルを使って話を進めていく。

module M1
    extend ActiveSupport::Concern
    included {}
end

module M2
    extend ActiveSupport::Concern
    include M1
    class_methods {}
end

class C
    include M2
end

また、この記事を書いていた時のRailsのバージョンは4.1.4。ソースコードgithubリポジトリ参照。 ActiveSuppor::Concernのソースコードを随時参照しながら見て欲しい。

モジュールM1

まず、モジュールM1の読み込み時に何が起こるのかを見ていく。

module M1
    extend ActiveSupport::Concern
    included {}
end
  1. extend ActiveSupport::Concernでは、extend_object, extendedメソッドが呼ばれる。
  2. extend_objectは特に変更もないので、Concernのメソッド(append_feature, included)がM1の特異クラスのメソッド(クラスメソッド)として追加される。 これにより、M2.include M1とした場合に、M1のappend_feature, includedが呼ばれる。 これはM1のメソッド探索の順番が、M1の特異クラス->M1のクラス(Moduleクラス)というようになっているため。
  3. Concern::extendedが呼ばれて @_dependencies に空配列が設定される。

モジュールM2

次に、モジュールM2にモジュールM1をincludeする

module M2
    extend ActiveSupport::Concern
    include M1
    class_methods {}
end
  1. M2にConcernがextendされる。M1と同様のため詳細は省く。
  2. include M1ではappend_featureとincludedが呼ばれる。
  3. append_featureが呼ばれるとき、引数にはM2が渡される。M2はConcernをextendしているので@dependenciesが定義されている。 そのため、@dependenciesにM1を追加してメソッドは終了。
  4. includedが呼ばれるとき、引数にはM2が渡される。else側の分岐に行き、superからModule#includedが呼ばれるため、特別な処理は行われない。
  5. class_methods doActiveSupport::Concern#class_methodsが呼ばれるのでそちらを見る。ClassMethodsが定義されていれば、それをレシーバに ブロックを評価、定義されていなければClassMethodsモジュールを作成してブロックを評価する。 今回はClassMethodsが定義されていないので、M2の内部にClassMethodsモジュールを作成しそこでブロックを評価する。

クラスC

最後にクラスCでモジュールM2をincludeする

class C
    include M2
end
  1. include M1ではappend_featureとincludedが呼ばれる。
  2. M1のappend_featureが呼ばれるとき、引数にはCが渡される。CはConcernをextendしていないので、分岐のelse側に入る。 @_dependenciesに含まれているモジュール(今回はM1)をCにincludeする。このincludeでもappend_featureが呼ばれるため、再帰的にincludeが行われる。
  3. superでModule#append_featureが呼ばれ、通常のincludeの処理が行われる。
  4. M2にClassMethodsがあるので、ClassMethodsをextendする。
  5. @included_blockが定義されている時、Cのコンテキストでブロックを実行する。今回で言えば、M1に@included_blockが定義されているため、M1をincludeした時に Cのコンテキストでブロックが評価される。
  6. M1のincludedが呼ばれるとき、引数にはM2が渡される。superからModule.includedが呼ばれるため、特別な処理は行われない。