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