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
extend ActiveSupport::Concern
では、extend_object, extendedメソッドが呼ばれる。- extend_objectは特に変更もないので、Concernのメソッド(append_feature, included)がM1の特異クラスのメソッド(クラスメソッド)として追加される。
これにより、
M2.include M1
とした場合に、M1のappend_feature, includedが呼ばれる。 これはM1のメソッド探索の順番が、M1の特異クラス->M1のクラス(Moduleクラス)というようになっているため。 - Concern::extendedが呼ばれて @_dependencies に空配列が設定される。
モジュールM2
次に、モジュールM2にモジュールM1をincludeする
module M2 extend ActiveSupport::Concern include M1 class_methods {} end
- M2にConcernがextendされる。M1と同様のため詳細は省く。
include M1
ではappend_featureとincludedが呼ばれる。- append_featureが呼ばれるとき、引数にはM2が渡される。M2はConcernをextendしているので@dependenciesが定義されている。 そのため、@dependenciesにM1を追加してメソッドは終了。
- includedが呼ばれるとき、引数にはM2が渡される。else側の分岐に行き、superからModule#includedが呼ばれるため、特別な処理は行われない。
class_methods do
でActiveSupport::Concern#class_methodsが呼ばれるのでそちらを見る。ClassMethods
が定義されていれば、それをレシーバに ブロックを評価、定義されていなければClassMethods
モジュールを作成してブロックを評価する。 今回はClassMethodsが定義されていないので、M2の内部にClassMethodsモジュールを作成しそこでブロックを評価する。
クラスC
最後にクラスCでモジュールM2をincludeする
class C include M2 end
include M1
ではappend_featureとincludedが呼ばれる。- M1のappend_featureが呼ばれるとき、引数にはCが渡される。CはConcernをextendしていないので、分岐のelse側に入る。 @_dependenciesに含まれているモジュール(今回はM1)をCにincludeする。このincludeでもappend_featureが呼ばれるため、再帰的にincludeが行われる。
- superでModule#append_featureが呼ばれ、通常のincludeの処理が行われる。
- M2にClassMethodsがあるので、ClassMethodsをextendする。
- @included_blockが定義されている時、Cのコンテキストでブロックを実行する。今回で言えば、M1に@included_blockが定義されているため、M1をincludeした時に Cのコンテキストでブロックが評価される。
- M1のincludedが呼ばれるとき、引数にはM2が渡される。superからModule.includedが呼ばれるため、特別な処理は行われない。