Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Elegant way to prepend to a module which is already included?

Tags:

ruby

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?

like image 483
Daniel Rikowski Avatar asked Mar 10 '19 18:03

Daniel Rikowski


People also ask

What's the difference between extend prepend and include?

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.

What is included do in Ruby?

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 Ruby?

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).

How do I add a module in Rails?

You can include a module in a class in your Rails project by using the include keyword followed by the name of your module.


1 Answers

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.

like image 180
Stefan Avatar answered Oct 30 '22 04:10

Stefan