ActiveSupport::Concern のソースコードリーディング #1 復習編
Rails で複数のモデルに共通するメソッドをモジュールにまとめたいときに活躍するActiveSupport::Concern というライブラリがあります。
ActiveSupport::Concern
https://github.com/rails/rails/blob/master/activesupport/lib/active_support/concern.rb
この ActiveSupport::Concern がどうような実装になっているのか気になりましたので、少し調べたことを書き残しておきます。調べたことを1つの記事で書こうと思ったのですが、文章が長くなりそうでしたので、2つの記事に分けて書くことにします。
ソースコードリーディング前の復習
ActiveSupport::Concern は Ruby の include メソッドと extend メソッドを仕様をうまく利用して実装されています。ソースコードリーディングを始める前に、include メソッドと extend メソッドについて復習しておきます。(ちなみに、今回の記事は復習だけで終わります。)
include
クラスやモジュール内で include メソッドを使うと、指定したモジュールに含まれるメソッドや定数を取り込むことができます。
module M def foo puts "foo" end end class C include M end C.new.foo # => foo
モジュールに append_features クラスメソッドが定義されている場合は、include するときに append_features クラスメソッドが呼び出されます。るりまによると、include メソッドの実体は append_features クラスメソッドだそうです。
参考: instance method Module#append_features
module M def foo puts "foo" end def self.append_features(klass) puts "#{klass} include #{self}." end end class C include M # include したときに M.append_features が呼び出されるので、以下が出力される # => "C include M." end C.new.foo # => NoMethodError
モジュール M に append_features クラスメソッドを定義してしまったので、include メソッドの本来の処理である「モジュールのメソッドや定数を取り込む」という処理が行われていません。なので、C クラスの foo インスタンスメソッドを呼び出したときに、NoMethodError が返ってきています。このエラーは super を使うことで解決することができます。
module M def foo puts "foo" end def self.append_features(klass) puts "#{klass} include #{self}." super end end class C include M # => "C include M." end C.new.foo # => "foo"
期待通り、C クラスに foo インスタンスメソッドが定義されていますね。
さらに、モジュールに included クラスメソッドが定義されている場合は、include したあとに included クラスメソッドが呼び出されます。
module M def self.append_features(klass) puts "#{klass} include #{self}." super end def self.included(klass) puts "#{klass} has included #{self}." end end class C include M # => "C include M." # => "C has included M" end
include メソッドについて復習しておくことは以上です。
extend
Ruby には「特異クラス」というオブジェクト一つ一つに割り当てられた特別なクラスが存在します。class << obj という構文を使って、この特異クラスにメソッドを定義することができます。
foo = "私のブログ" bar = "あなたのブログ" class << foo def add_author(name) "#{self} 著者: #{name}" end end puts foo.add_author("odailly_jp") # => "私のブログ 著者: odailly_jp" puts bar.add_author("odailly_jp") # => NoMethodError
上記のコードでは、オブジェクト foo の特異クラスにだけ add_author メソッドを定義しましたので、オブジェクト bar では add_author メソッドを呼び出すことができませんでした。このように、特定のオブジェクトにだけ定義したいメソッドがある場合に特異クラスを利用します。ちなみに、特異クラスに定義したメソッドを「特異メソッド」と呼びます。
プログラミンングをしていると「モジュールに含まれるメソッドをオブジェクトの特異メソッドとして定義したい!」と思うときがあります。このときに使うのが extend メソッドです。extend メソッドを使うと、指定したモジュールに含まれるメソッドをオブジェクトの特異メソッドとして取り込むことができます。
module M def foo puts "foo" end end obj = Object.new obj.extend(M) obj.foo # => foo
extend メソッドはクラスやモジュール内で次のように使うこともできます。
module M def foo puts "foo" end end class C extend M end C.foo # => foo
Ruby の全てのクラスは Class クラスのインスタンスオブジェクトなので、このようなことができるみたいです。(この辺りのことは『メタプログラミング Ruby』に優しく説明されていますので、そちらを読むことをお勧めします。)
難しいと感じた方は、今は「クラスやモジュールの中で extend を使うと、指定したモジュールに含まれるインスタンスメソッドをクラスメソッドとして取り込むことができる。」と思ってください。ActiveSupport::Concern のソースコードを読むときは、このぐらいの理解でも大丈夫だと思います。
モジュールに extend_object クラスメソッドが定義されている場合は、extend するときに extend_object クラスメソッドが呼び出されます。るりまによると、extend メソッドの実体は extend_object クラスメソッドだそうです。include メソッドと append_features クラスメソッドの関係と同じですね。
参考: instance method Module#extend_object
module M def foo puts "foo" end def self.extend_object(klass) puts "#{klass} extend #{self}." super end end class C extend M # => "C extend M." end C.foo # => "foo"
さらに、モジュールに extended クラスメソッドが定義されている場合は、extend したあとに extended クラスメソッドが呼び出されます。include メソッドと included クラスメソッドの関係と同じですね。
module M def self.extend_object(klass) puts "#{klass} extend #{self}." super end def self.extended(klass) puts "#{klass} has extended #{self}." end end class C extend M # => "C extend M." # => "C has extended M" end
説明が長くなってしまいましたが、extend メソッドについて復習しておくことは以上です。(特異クラスの説明を『メタプログラミング Ruby』に丸投げしてだいぶ端折っていますが、ご了承ください。)
復習編のまとめ
include | extend | |
---|---|---|
何をするメソッドか? | メソッドや定数を取り込む | メソッドを特異メソッドとして取り込む |
実行時に呼び出されるメソッド | append_features | extend_object |
終わった後に呼び出されるメソッド | included | extended |
以上、続きます。