I want to monkey-patch a gem and the targeted code is in a module. Unfortunately, at the time when I'm prepending my patch, the module has already been included in various classes and the new code has no effect.
Example:
module Feature
def action
puts "Feature"
end
end
module Patch
def action
puts "Patch"
end
end
class Base1
include Feature
end
Feature.prepend Patch
class Base2
include Feature
end
Base1.new.action # Returns "Feature", I want it to be "Patch" instead.
Base2.new.action # Returns "Patch"
When I prepend to Feature before it is included into Base2 the patch works, but with the real gem I cannot change the order.
Is there an elegant way to solve this or do I have to traverse ObjectSpace to find which classes already include the Feature module?
The only difference is where in the ancestor chain the module is added. With include , the module is added after the class in the ancestor chain. With prepend, the module is added before the class in the ancestor chain.
included is called when you include module into a class, it is used for defining relations, scopes, validations, ... It gets called before you even have created object from that class.
What is the difference between a class and a module? Modules are collections of methods and constants. They cannot generate instances. Classes may generate instances (objects), and have per-instance state (instance variables).
You can include a module in a class in your Rails project by using the include keyword followed by the name of your module.
TL;DR – you can't in general, but Base1.include Patch may be good enough.
For your example code, the ancestors of Base1 and Base2 are: (aligned for clarity)
Base1.ancestors #=> [Base1, Feature, Object, Kernel, BasicObject]
Base2.ancestors #=> [Base2, Patch, Feature, Object, Kernel, BasicObject]
Base2 has an additional ancestor Patch before Feature – the result of Feature.prepend Patch.
Ruby doesn't allow us to freely modify a module's ancestors chain, so we can't just prepend Patch to Feature retroactively.
But fortunately, Patch is the first module after the Base class, so we can resort to include to append Patch to Base1 instead:
Base1.include Patch
Base1.ancestors #=> [Base1, Patch, Feature, Object, Kernel, BasicObject]
Obviously, this only works for very specific cases and not in general.
Here's a counter example:
module Feature
def action ; 'Feature' ; end
end
module Foo
def action ; "#{super} overridden" ; end
end
module Patch
def action ; 'Patch' ; end
end
class Base1
include Feature
include Foo
end
Feature.prepend(Patch)
class Base2
include Feature
include Foo
end
Base1.new.action #=> "Feature overridden"
Base2.new.action #=> "Patch overridden"
Base1.include Patch
Base1.new.action #=> "Patch"
Looking at the ancestors reveals the problem:
Base1.ancestors #=> [Base1, Foo, Feature, Object, Kernel, BasicObject]
Base2.ancestors #=> [Base2, Foo, Patch, Feature, Object, Kernel, BasicObject]
Base1.include Patch
Base1.ancestors #=> [Base1, Patch, Foo, Feature, Object, Kernel, BasicObject]
Patch and Foo are out of order.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With