I have some code based on this SO answer: https://stackoverflow.com/a/2136117/2158544. Essentially it looks like this (note: in the actual code, I do not control module 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
First inclusion:
class C
include A
include B
end
A::ClassMethods # <- output
Second inclusion:
class D
include A
include B
end
B::ClassMethods # <- output
Although the call to super
still gets correctly routed to module A
, I'm confused why singleton_test
is already "wrapped" when it gets included into class D
(owner is B::ClassMethods
). My theory is that it's because when module B
redefines singleton_test
, it's redefining it on the included module level (module A
) and thus every time module A
gets included subsequently, the method has already been "wrapped".
When you call a method ruby walks up the ancestral chain looking for the method, if it reaches the top (BasicObject) and cannot find a matching method it will throw an error (unless method_missing
is defined)
How this works in your example
First Inclusion
C.singleton_class.ancestors
#=> [#<Class:C>, B::ClassMethods, A::ClassMethods, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
When you lookup singleton_test
it checks the ancestral chain
singleton_test
Now in the B::ClassMethods::extended
hook you are defining singleton_test
using define_method
. Since you called this method without a receiver the implicit receiver is self
and self
in the context of this method is B::ClassMethods
so in essence you are calling
module B
module ClassMethods
def singleton_test(*args,&blk)
super(*args, &blk)
end
end
end
You can see this more clearly as
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)}"
Output:
Before B inclusion: []
After B inclusion: [:singleton_test]
Second Inclusion
I think you can see where this is going
D.singleton_class.ancestors
#=> [#<Class:D>, B::ClassMethods, A::ClassMethods, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
When you lookup singleton_test
it checks the ancestral chain
singleton_test
So it is not that the method is "already wrapped" or that the method is being redefined on A
it is that you have defined a new method in B::ClassMethods
and since B
is included after A
it's definition takes priority (overrides that of A
in this context).