モジュールの特異メソッドをincludeして使えるようにする

次のようなFooモジュールの特異メソッドbarをBazモジュールに特異メソッドとして使えるようにしたい場合、BazでFooをincludeするだけでは不十分。これは、includeによって追加されるのが、Fooのインスタンスメソッドのみだから。

module Foo
  def bar
    puts "bar"
  end
end

module Baz
  include Foo
end

なので、次のように細工をしてやる。

module Foo
  # Module#extendのためにインスタンスメソッドにする
  def bar
    puts "bar"
  end

  # self(この場合はFoo)が他のモジュール(仮引数mod == Baz)にincludeされた際に呼ばれる
  def self.included(mod)
    # modにself(この場合はFoo)のインスタンスメソッドを特異メソッドとして追加する。
    # mod == Bazだから、つまりBazにクラスメソッドを追加することになる
    mod.extend self
  end
end

module Baz
  include Foo
end

このように、Module.includedというフックメソッドを用いて、includeされた際にModule.extendでインスタンスメソッドを特異メソッドとして追加してやればよい。

このようなやり方は、ActiveRecord::Validations::ClassMethodsをActiveRecord::Baseから利用できるようにする際などに利用されている。

追記1 2009-10-17

上記の方法だと、Baz#bar(Bazのインスタンスメソッド)とBaz.bar(クラスメソッド)の両方が定義される。Bazをモジュールではなくクラスとして作成した場合には注意が必要となる。

一応、以下のようにすることでBaz#barを未定義にできる。

module Baz
  include Foo
  # Fooのメソッドを直接指定して未定義とする
  # 直接指定なあたりがちょっとかっこわるい
  undef_method :bar

  # 当然ながら(?)このあとにbarを再定義すれば、そちらが利用される
  # まぁ再定義するぐらいならundef_methodの必要がない気もするけど。
  def bar
    puts "hello"
  end

undef_methodだとスーパークラスでの定義さえもみなくなってしまうので、できればremove_methodがいいんだけど、それだとBaz#barが定義されていないというエラーが出た。

追記2 2009-10-18

先のBaz#barとBaz.barについて、includeしたせいで両方定義されてしまうというのは、extendだけ行えば済むことかとやっと気づいた。つまり、

module Foo
  def bar
    puts "bar"
  end
end

module Baz
  extend Foo
end

ActiveRecord::ValidationsだけでなくActiveRecord::Validations::ClassMethodがあるのは、includeとextendで別個に分けるためだったのか…。

追記3

恐ろしく分かりにくい記事なので、書き直した

参考資料