3

I have a Ruby class C that includes some third-party modules, say A and B.

Module A is included indirectly via C's class inheritance chain; assume I have no control over where A gets incuded. Now, C includes B directly, but B includes another module D which happens to provide functionality that is also provided by A, like this:

class C < Base

  # Base includes A

  include B   # includes D

  # methods in A overridden by D

end      

The ancestor chain goes something like this (where ... represents zero or more other ancestors that aren't relevant to this discussion):

C ... B ... D ... A 

I want the functionality of A to take precdence over D: I want to move A so it is in front of D in the ancestor chain, like this:

C ... A ... B ... D

I have tried simply including A again but this didn't work. Is there a way to do this?

starfry
  • 9,273
  • 7
  • 66
  • 96
  • did you look into [`Module#prepend`](http://ruby-doc.org/core-2.3.0/Module.html#method-i-prepend)? – Myst Apr 22 '16 at 17:48
  • P.S. I meant, prepend `A` and then include `B`.... Also - won't changing the hierarchy risk breaking `B`'s implementation (which might rely on the original inheritance flow)? – Myst Apr 22 '16 at 17:53
  • There would be that risk, but in this particular scenario I know the risk isn't there. I only have the problem because B includes something it shouldn't. – starfry Apr 22 '16 at 18:19
  • Why the rush to select an answer? It also seems odd to select an answer without upvoting it, but to each their own. – Cary Swoveland Apr 22 '16 at 18:49

3 Answers3

2

It is impossible to change the mixin hierarchy once it is established. And only the inclusion order determines the hierarchy. You have to include A into C (for the first time) after you include B into C, or, if you prepend A to C instead of including it, then it will have precedence over D even if B is included into C later.

sawa
  • 165,429
  • 45
  • 277
  • 381
  • I've added a class example to the question. I've tried many permutations of `include A` and `prepend A` without success. – starfry Apr 22 '16 at 17:45
  • 1
    You cannot include or prepend a module multiple times to the same class. As I wrote, the timing of inclusion/prepending **for the first time** matters. – sawa Apr 22 '16 at 17:50
  • Ah I get you, it's that **first time** thing. That's what I thought and I ended up drawing the conclusion that you confirm, that it can't be done. – starfry Apr 22 '16 at 18:21
1

Of course you can. I do it daily and twice on Sundays. Well, sort of...

module A
  def hiya(str)
    puts "ho #{str}"
  end
  def if_what?
  end
end

module D
  def hiya(str)
    puts "hi #{str}"
  end
  def what_if?
  end
end

module B
  include D
end

class C
  include A
  include B
end

As expected:

C.ancestors
  #=> [C, B, D, A, Object, Kernel, BasicObject] 
a = C.new
  #=> #<C:0x007fc56324ed40> 
a.hiya("Lois")
hi Lois

To invoke A's instance methods instead of D's we can write:

(A.instance_methods & D.instance_methods).each { |m| D.send(:remove_method, m) }

Let's see:

D.instance_methods
  #=> [:hiya, :what_if?] 
(A.instance_methods & D.instance_methods).each { |m| D.send(:remove_method, m) }
  #=> [:hiya] 
D.instance_methods
  #=> [:what_if?] 
C.instance_methods.include?(:hiya)
  #=> true 
a.hiya("Lois")
ho Lois
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • I've tried this and it does work, but it doesn't alter the ancestor chain - it removes the method from the module, which affects anything else that uses it. Is there a way of achieving the effect within the scope of class `C` so that any other classes including `D` aren't affected ? – starfry Apr 24 '16 at 13:09
  • That's what I meant by, "Well, sort of...". @sawa had already explained that the ancestor chain cannot be rearranged (the answer to your question), so there was nothing more to be said about that.I merely showed you something to you could do that would have the same effect as what you want to do. I don't understand what you mean by other classes within the scope of class `C`. Classes cannot contain classes. – Cary Swoveland Apr 24 '16 at 16:37
  • What I meant was could the method removal from `D`l be limited to its inclusion in class `C` so that other classes including `D` aren't affected.I just asked in case you knew of something but I realize that it's the same `D` in all classes' ancestor chains and that a change to `D` would affect all classes having `D` as an ancestor. – starfry Apr 24 '16 at 16:59
  • You are correct that removing a method from `D` makes it unavailable to all classes that include `D`, regardless of where the removal takes place. One possibility would be to dynamically create methods in `C` that override methods in `D`, invoking a method with the same name in a more distant module in the ancestor chain. – Cary Swoveland Apr 24 '16 at 18:06
  • You might also have a look at [this question](http://stackoverflow.com/questions/23739582/does-calling-super-cause-further-methods-in-the-parent-class-to-be-used) and the discussion I had with @phrogz in the comments. – Cary Swoveland Apr 24 '16 at 19:49
  • I'm not sure what I was thinking in my first comment, but classes may certainly contain classes. – Cary Swoveland Apr 30 '16 at 07:51
1

You can almost do this. You can't move one class/module up the hierarchy, but you can clone a module and insert that anonymous module clone in front of the other ancestors. Check this out:

module A
  def salutations
    "a"
  end
end

module D
  def salutations
    "d"
  end
end

module B
  include D
end

class Base
  include A
end

class C < Base
  include B
end

c = C.new
puts c.salutations

#=> d

So far, so good.

And now:

module A
  def salutations
    "a"
  end
end

module D
  def salutations
    "d"
  end
end

module B
  include D
end

class Base
  include A
end

class C < Base
  include B
  def initialize
    aye = A.dup
    self.class.include aye
  end
end

c = C.new
puts c.salutations

#=> a

This won't work if the module you are cloning is going to be modified later, of course.

Cyno
  • 11
  • 1