読者です 読者をやめる 読者になる 読者になる

1つのモジュールの中で特異メソッドとインスタンスメソッドを定義して、クラスに引き渡す方法

active supportを使うと、次のようになります。

require 'active_support/concern'
module M
  extend ActiveSupport::Concern
  def method2
    puts "method2"
  end
  module ClassMethods
    def method1
      puts "method1"
    end
  end
end
 
class C 
  include M 
end
C.method1 #=> method1
C.new.method2 #=> method2

これはextendとincludeを組み合わせてやっています。

METHOD_LIST = []
module XXXMethod
  (1..3).each do|i| 
    fn = "method#{i}"
    METHOD_LIST << fn.to_sym
    define_method(fn) do
      if self.class.to_s == "Class"
        puts "static method:#{fn}"
      else
        puts "instance method:#{fn}"
      end
    end
  end
end

class UseXXXAsStaticMethod
  extend XXXMethod
end
class UseXXXAsInstanceMethod
  include XXXMethod
end

obj =  UseXXXAsInstanceMethod.new
METHOD_LIST.each do|fn|
  UseXXXAsStaticMethod.__send__ fn 
  obj.__send__ fn
end

を実行してみると分かるのですが、extendはモジュールのメソッドをクラスの特異メソッドとして引き渡し、includeはインスタンスメソッドとして引き渡します。
Moduleの特異メソッドのappend_featuresはincludeされたときに呼ばれるメソッドで、ActiveSupport::Concernはこれを利用しています。まずappend_featuresの動作を次のコードで確認します。

module M 
  def self.append_features(base)
    puts "self:#{self}, base:#{base}"
    super
  end
end
class C
  include M
end
#=> self:M, base: C

それでは、Concernの簡易版を作ります。ConcernをextendしたモジュールMは、クラスCにincludeされると、Concernで定義したappend_features(base)が呼ばれます。ここで、baseはCになっているので、base.extend StaticMethodsすることで、特異メソッドを定義し、superでインスタンスメソッドを引き渡せます(superはincludeのデフォルトの動作)。

module Concern 
  def append_features(base)
    base.extend const_get("StaticMethods") if const_defined?("StaticMethods")
    super
  end
end
module M
  extend Concern 
  def say_hello 
    puts "hello"
  end
  module StaticMethods
    def method1 
      puts "1" 
    end
  end
end
module StaticMethods
  def method1
    puts "2"
  end
end
class C
  include M
end
C.method1 #=>1
C.new.say_hello #=> hello