Search code examples
rubyinheritancedecorator

Behaviour of `prepend` in Ruby class hierarchies


I have a class Base, and two classes Derived and Derived2 that inherit from Base. They each have a function foo defined in them.

I also have a module Gen which is prepend-ed to Base. It is also prepend-ed to Derived2 but not to Derived.

When I call foo on an instance of Derived2, the result is as if the Gen module was only prepend-ed to Base and not to Derived2 also. Is this the expected behavior?

Here is the code for the above scenario:

module Gen
  def foo
    val = super
    '[' + val + ']'
  end
end

class Base
  prepend Gen

  def foo
    "from Base"
  end
end

class Derived < Base
  def foo
    val = super
    val + "from Derived"
  end
end

class Derived2 < Base
  prepend Gen
  def foo
    val = super
    val + "from Derived"
  end
end

Base.new.foo     # => "[from Base]"

Derived.new.foo  # => "[from Base]from Derived"

Derived2.new.foo # => "[from Base]from Derived"

I expected the last of the above statement to output:

[[from Base]from Derived]

Solution

  • To help you understand, there is a method Class#ancestors, which tells you the order in which a method will be searched for. In this case:

    Base.ancestors     # => [Gen, Base, Object, Kernel, BasicObject]
    Derived.ancestors  # => [Derived, Gen, Base, Object, Kernel, BasicObject]
    Derived2.ancestors # => [Gen, Derived2, Gen, Base, Object, Kernel, BasicObject]
    

    So when you call a method on an object that is an instance of said class, that method will be searched in the corresponding list in that order.

    • Prepending puts a module in front of that list.
    • Inheriting puts the parent's chain at the end of the child's (not exactly, but for simplicity's sake).
    • super just says "go traverse the chain further and find me the same method".

    For Base, we have two implementations of foo - that in Base and that in Gen. The Gen one will be found first as the module was prepended. Therefore calling it on an instance of Base will call Gen#foo =[S], which will search up the chain as well (via super) =[from Base].

    For Derived, the module wasn't prepended, and we have inheritance. Therefore, the first found implementation is that in Derived =Sfrom Derived and super will search the rest of the chain that comes from Base (aka the above paragraph is played out) =[from Base]from Derived.

    For Derived2, the module is prepended, so the method there will be found first =[S]. Then the super there will find the next foo in Derived2 =[Sfrom Derived], and the super there will play out the situation for Base again =[[from Base]from Derived].


    EDIT: It seems that up until very recently, prepend would first search in the ancestors chain and add the module only if it is not already present (similarly to include). To make it even more confusing, if you first create the parent, inherit from it, prepend in the child and then prepend in the parent, you will get the result of newer versions.