Rubyではモジュールレベルのメソッドオーバーライドはどのように機能しますか?
概要
このSOの回答に基づいたコードがいくつかあります: https://stackoverflow.com/a/2136117/2158544。基本的には次のようになります (注: 実際のコードでは、モジュール A を制御しません)。
module A
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def singleton_test
end
end
end
module B
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def self.extended(base)
puts base.method(:singleton_test).owner
define_method(:singleton_test) do |*args, &blk|
super(*args, &blk)
end
end
end
end
最初の包含:
class C
include A
include B
end
A::ClassMethods # <- output
2 番目に含まれるもの:
class D
include A
include B
end
B::ClassMethods # <- output
super への呼び出しは依然としてモジュール A に正しくルーティングされますが、singleton_test がクラス D (所有者は B::ClassMethods) に含まれるときにすでに「ラップ」されている理由がわかりません。私の理論では、モジュール B が singleton_test を再定義すると、含まれるモジュール レベル (モジュール A) で再定義されるため、その後モジュール A が含まれるたびにメソッドがすでに「ラップ」されているためです。
解決策
メソッドを呼び出すと、Ruby は祖先チェーンをたどってメソッドを探します。最上位 (BasicObject) に到達しても一致するメソッドが見つからない場合は、エラーがスローされます (method_missing が定義されていない限り)。
あなたの例でこれがどのように機能するか
最初の包含
C.singleton_class.ancestors
#=> [#<Class:C>, B::ClassMethods, A::ClassMethods, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
singleton_test を検索すると、祖先チェーンがチェックされます。
ここで、B::ClassMethods::extended フックで、define_method を使用して singleton_test を定義しています。レシーバーなしでこのメソッドを呼び出したので、暗黙的なレシーバーは self であり、このメソッドのコンテキストにおける self は B::ClassMethods であるため、本質的には呼び出していることになります。
module B
module ClassMethods
def singleton_test(*args,&blk)
super(*args, &blk)
end
end
end
これをより明確に見ることができます
puts "Before B inclusion: #{B::ClassMethods.instance_methods(false)}"
class C
include A
include B
end
puts "After B inclusion: #{B::ClassMethods.instance_methods(false)}"
出力:
Before B inclusion: []
After B inclusion: [:singleton_test]
2 番目の包含
これがどこに向かうのかわかると思います
D.singleton_class.ancestors
#=> [#<Class:D>, B::ClassMethods, A::ClassMethods, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
singleton_test を検索すると、祖先チェーンがチェックされます。
したがって、メソッドが「すでにラップされている」ということではなく、メソッドが A で再定義されているということではなく、B::ClassMethods で新しいメソッドを定義しており、B が A の後に含まれているため、その定義が優先されます(その定義はオーバーライドされます)。この文脈では A)。