2011.03.01
■[Ruby] self.included(Module#included)と module ClassMethods の組み合わせ
実践 Rails を読んでいて、モジュールからインクルードして使えるようにするクラスメソッドを以下のように定義できることと、Module#included を知った。以下は、モジュール Foo をインクルードしたクラス Bar で、fn を自身のクラスメソッドとして使うためのイディオム。
module Foo def self.included(klass) klass.extend(ClassMethods) end module ClassMethods def fn; 'hello' end end end class Bar include Foo end Bar.fn # => "hello"
Module の self.included の解説は以下が参考になる。
- http://www.ruby-lang.org/ja/man/html/Module.html
- http://d.hatena.ne.jp/zariganitosh/20080817/1219021965
最初、ClassMethods というモジュールがもともとあってそれをオープンしているのかと思ったけど、self.included がうまく働いているだけで、次のようにも書ける。
module Foo def self.included(klass) klass.extend(KoremoClassMethods) end module KoremoClassMethods def fn2; 'hello2' end end end class Bar include Foo end Bar.fn2 # => "hello2"
たんなるモジュールの入れ子なのだから、これだっていい。
module Foo # def self.included(klass) # klass.extend(ClassMethods) # end module ClassMethods def fn; 'hello' end end end class Bar #include Foo extend Foo::ClassMethods end Bar.fn # => "hello"
そもそも extend は extend が書かれるクラスの特異クラスにモジュールをインクルードするためのショートカットなのだから、次のような素直な書き方で、モジュールのメソッドは普通にエクステンド先のクラスメソッドになる。
module Foo def fn; 'hello' end end class Bar extend Foo end Bar.fn # => "hello"
なぜこのような書き方ではなく、わざわざ "module Foo ... module ClassMethods ..."という使い方をするかというと、その方が Module#included を使ってモジュールをうまく構造化できるから。以下の例では、モジュール Foo がインクルードされたとき、そのサブモジュール ClassMethods が「インクルード」されるため、インクルード先のクラスメソッドにはならない。このように Module#included は、extend ではなく include するのにも用いられる。
module Foo def self.included(klass) #klass.extend(ClassMethods) klass.__send__ :include, ClassMethods #補足.プライベートだから__send__で呼びだす end module ClassMethods def fn; 'hello' end end end class Bar include Foo end Bar.new.fn # => "hello" Bar.fn # => # ~> -:19:in `<main>': undefined method `fn' for Bar:Class (NoMethodError)
と、ここまで考えてようやく、Moduleのincludeとかextendとか - チナミニ に載っていた以下のコードがすんなり頭に入った。
module Foo def self.included(klass) klass.extend(ClassMethods) klass.__send__ :include, InstanceMethods end module ClassMethods #クラスメソッドを置くための名前空間としてのモジュール def fn; 'hello' end end module InstanceMethods #インスタンスメソッドを置くための名前空間としてのモジュール def fn2; 'hello2' end end end class Bar include Foo end Bar.fn # => "hello" Bar.new.fn2 # => "hello2"
