(2015年までの)odaillyjp blog

イベント参加記録とプログラミング系の雑記

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 は Rubyinclude メソッド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

以上、続きます。