こんにちは、鈴木です。
先日、「ActiveSupport::Concern でハッピーなモジュールライフを送る」で ActiveSupport::Concern をご紹介しましたが、きっと裏側では色々なテクニックが使われているのではないでしょうか。
ということで ActiveSupport::Concern が裏側でどのような処理を行なっているのか、ActiveSupport::Concern のコードを読んでみます!
ActiveSupport::Concern のコードを読む
ソースコードはこのようになっていました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
module ActiveSupport ... module Concern def self.extended(base) base.instance_variable_set("@_dependencies", []) end def append_features(base) if base.instance_variable_defined?("@_dependencies") base.instance_variable_get("@_dependencies") << self return false else return false if base < self @_dependencies.each { |dep| base.send(:include, dep) } super base.extend const_get("ClassMethods") if const_defined?("ClassMethods") if const_defined?("InstanceMethods") base.send :include, const_get("InstanceMethods") ActiveSupport::Deprecation.warn "The InstanceMethods module inside ActiveSupport::Concern will be " \ "no longer included automatically. Please define instance methods directly in #{self} instead.", caller end base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block") end end def included(base = nil, &block) if base.nil? @_included_block = block else super end end end ... end |
extended, append_features, included という 3 つのメソッドが定義されていますね。
extended
最初に extended から見ていきましょう。
1 2 3 |
def self.extended(base) base.instance_variable_set("@_dependencies", []) end |
extended はモジュールが extend された時に呼び出されるメソッドですが、ここではbase に対して変数 @_dependencies を設定しています。
変数名から想像すると、依存関係に関する何かが @_dependencies に保持されるのでしょう。
included
次に included を見てみましょう。
1 2 3 4 5 6 7 |
def included(base = nil, &block) if base.nil? @_included_block = block else super end end |
base が nil かどうかで分岐しています。
base が nil になるのは、以下のように呼び出された場合です。
1 2 3 4 5 6 |
module Example extend ActiveSupport::Concern included do ... end end |
この時はブロックを @_included_block に保存しています。
base が nil 以外になるのは、どのような場合でしょうか。それは以下のように ActiveSupport::Concern で extend したモジュールが include された場合です。
1 2 3 |
module OtherModule include Example end |
このときは特別な処理は行わず、単純に元の処理(super)を呼び出しています。
append_features
最後は append_features です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
def append_features(base) if base.instance_variable_defined?("@_dependencies") base.instance_variable_get("@_dependencies") << self return false else return false if base < self @_dependencies.each { |dep| base.send(:include, dep) } super base.extend const_get("ClassMethods") if const_defined?("ClassMethods") if const_defined?("InstanceMethods") base.send :include, const_get("InstanceMethods") ActiveSupport::Deprecation.warn "The InstanceMethods module inside ActiveSupport::Concern will be " \ "no longer included automatically. Please define instance methods directly in #{self} instead.", caller end base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block") end end |
append_features はあまり見慣れないメソッドですが、ActiveSupport が定義するメソッドではなく、Ruby のメソッドです(see Module#append_features)。
若干ボリュームのあるメソッドですが、@_dependencies が定義されているかどうかで条件分岐していますね。
どちらに分岐するのか
例として、以下のコードで考えてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
module M1 extend ActiveSupport::Concern end module M2 extend ActiveSupport::Concern include M1 # (1) end module M3 extend ActiveSupport::Concern include M2 # (2) end class C include M3 # (3) end |
include している場所は 3 箇所、コメントで (1), (2), (3) と書いているところですが、(1) と (2) については M1 と M2 は ActiveSupport::Concern で extend していますので、@_dependencies が定義されている場合の分岐に入ります。
(3) については、C は ActiveSupport::Concern で extend していませんので、@_dependencies が定義されていない場合の分岐に入ります。
それでは、条件分岐のそれぞれの処理を見ていきましょう。
@_dependencies が定義されている場合
この時は @_dependencies に self を追加しているだけです。
デフォルトの処理(super)を呼び出していないので、この時点で base にメソッドは追加されません。
※append_features のデフォルトの動作は「クラスやモジュール(base)に self の機能を追加する」であり、それを行わずに return しているからです。
@_dependencies が定義されていない場合
最初に「base < self」であるかチェックしています。
1 |
return false if base < self |
「base < self」は base の継承階層の上位に self が存在する場合に true となります。
これはモジュールが複数回 include された場合を考慮したものであり、2 回目以降は何も行わないためのチェックです。
次の行を見ると、@_dependencies に蓄えられているモジュールを順番に base に include しています。
1 |
@_dependencies.each { |dep| base.send(:include, dep) } |
@_dependencies に保持していたモジュールを順番に base に include しています。
以下のコードでいえば、M1 を M2 が include して、M2 を M3 が include して、M3 を C が include していますが、上記の処理で C に対して M1, M2 が include されることになります。そして注意ですが、この時点で M3 は C に include されません。というのも、@_dependencies に M3 は含まれていないからです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
module M1 extend ActiveSupport::Concern end module M2 extend ActiveSupport::Concern include M1 # (1) end module M3 extend ActiveSupport::Concern include M2 # (2) end class C include M3 # (3) end |
次の行では super を呼び出しています。ここでやっと、M3 が C に include されます(※正確には「M3 の機能が C に追加」されます。include すると append_features が呼び出され、append_features のデフォルトの処理は「base に self の機能を追加」されるので。ややこしい・・)。
1 |
super |
次は ClassMethods が定義されていれば、それで base を extend (クラスメソッドに追加)しています。
1 |
base.extend const_get("ClassMethods") if const_defined?("ClassMethods") |
その次は InstanceMethods が定義されていれば、それを base に include します。(※InstanceMethods を定義する方法は非推奨となったため、警告メッセージが表示されます。新しいコードを書く場合は、普通にモジュール内にメソッド定義します)。
1 2 3 4 5 |
if const_defined?("InstanceMethods") base.send :include, const_get("InstanceMethods") ActiveSupport::Deprecation.warn "The InstanceMethods module inside ActiveSupport::Concern will be " \ "no longer included automatically. Please define instance methods directly in #{self} instead.", caller end |
最後は included で指定されたブロックを base の class_eval に渡しています。
1 |
base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block") |
まとめ
ActiveSupport::Concern がどのように実装されているのか見てきましたが、いかがでしたでしょうか。
append_features を使っているコードを見るのは初めてでしたし、読み解くのは大変でしたが、得るものは大きかったです。
Happy Programming!