Hatena::ブログ(Diary)

わからん

2011.03.01

[] 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 の解説は以下が参考になる。


最初、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"

はてなユーザーのみコメントできます。はてなへログインもしくは新規登録をおこなってください。

Google