Ancestral Behavior
Ruby’s inheritance chain is simple: the last module included wins. Sometimes this can cause problems: what if a module you include happens to have a method that collides with a method from the superclass? It’s not a common problem, but when it does happen it’s a real head-scratcher. In this episode, you’ll learn how to solve it.
Video transcript & code
Let's say we've got a WashingMachine
class.
Given a WashingMachine
object, we have access to some typical washing machine settings, like the current water level, the current wash cycle , and the temperature in the tub.
wm = WashingMachine.new
wm.water_level # => 0
wm.cycle # => :fill
wm.temperature # => 72
Now let's say we develop a new washing machine, that builds on the old one.
class StainMaster5000 < WashingMachine
attr_accessor :clothes
def initialize
super
@clothes = ["red shirt", "blue sock", "black sock"]
end
end
Let's make an instance. This amazing new appliance can actually tell us what clothes are currently inside it!
sm = StainMaster5000.new
sm.clothes # => ["red shirt", "blue sock", "black sock"]
But why stop there? The StainMaster 5000 should have all the bells and whistles!
Let's include the Enumerable module, and then define the each
method to iterate through the clothes.
class StainMaster5000
# ...
include Enumerable
def each(&block)
clothes.each(&block)
end
end
Now we can do cool stuff like convert the machine directly to an array of clothes, sort the clothes , or even get a list of just the clothes' colors,, all directly from the washing machine object!
sm.to_a
# => ["red shirt", "blue sock", "black sock"]
sm.sort
# => ["black sock", "blue sock", "red shirt"]
sm.map{|g| g.split.first}
# => ["red", "blue", "black"]
OK, I admit it. This is one of the more contrived examples I've come up with on this show.
But I did it to highlight a problem that you may well run into some day, under less contrived circumstances.
Here's the problem. Let's take a second look back at the original WashingMachine
attributes.
sm.water_level # => 0
sm.cycle # => #<Enumerator: #<StainMaster5000:0x005610...
sm.temperature # => 72
Hmmm… one of these things is not like the others. The water_level
and temperature
readers still look right, but the cycle
indicator is now returning something very different than it used to.
The problem is, cycle is one of the methods that Enumerable defines. We can talk about what it does in another episode. The right now, it's just in the way.
What we'd like to do is to include all of the Enumerable methods, except any with names that conflict with base class methods. But that's not how Ruby module inheritance works. We can't be selective about which methods are brought in. It's all or nothing.
How do we say that this class should use the superclass definition of cycle
, instead of the Enumerable
definition? It's actually quite easy, but the solution is not at all obvious.
First off, we need to be able to acquire a handle on the original WashingMachine
definition of cycle
.
As we've seen in a few other episodes, we can do this by asking the WashingMachine
class for the instance_method
named :cycle
.
WashingMachine.instance_method(:cycle)
# => #<UnboundMethod: WashingMachine#cycle>
In return, we get an UnboundMethod
object.
Knowing this, let's return to the StainMaster5000
class definition.
After we include Enumerable, we send the define_method
message to the class.
We tell it we're defining the method named :cycle
.
Now typically, when we use define_method
, we pass in a block which will make up the body of the method. But not this time.
In this case, we pass a second argument, which is the UnboundMethod
we learned how to retrieve a moment ago.
class StainMaster5000
# ...
define_method(:cycle, WashingMachine.instance_method(:cycle))
end
When we re-test the reader methods, all is well again. cycle
once again returns :fill
.
sm.water_level # => 0
sm.cycle # => :fill
sm.temperature # => 72
This exploits an obscure capability of the define_method
call:
Instead of a block, it can take an UnboundMethod
object as a second argument. In effect, it acts almost like an aliasing mechanism: it gives us the ability to take method objects and plug them in under any name we want.
For instance, if we had wanted to leave the original Enumerable
definition intact, but we could have renamed the method :wash_cycle
instead.
class StainMaster5000
# ...
define_method(:wash_cycle, WashingMachine.instance_method(:cycle))
end
Admittedly, this is some deep metaprogramming, and hopefully you'll never need to use it. But it gives some insight into how methods in Ruby can be detached and then re-bound in other contexts.
Happy hacking!
Responses