In the the following tea timer code, there’s a ‘start’ method inside SleepTimer, which calls ‘notify.’
def start
sleep minutes * 60
notifier.notify("Tea is ready!")
end
If you look at the code below, you’ll see that there’s a notify method in the class StdioUi as well as a notify method in module UiWithBeep. The start method shown above calls the notify method in module UiWithBeep, which then, via ‘super,’ calls the notify method in class StdioUi. (The effect is that “BEEP!” is heard before “Tea is ready”.) However, I don’t understand why notifier.notify calls the notify method in module UiWithBeep rather than in class StdioUi.
First Question: how does it know to go to the one ‘notify’ over the other.
SecondQuestion And, although I understand super, what establishes the relationship so that notify in class StdioUi is ‘super’ to the other notify. Can you please explain
Tea Timer
class TeaClock
attr_accessor :timer
attr_accessor :ui
def initialize(minutes)
self.ui = StdioUi.new
self.timer = SleepTimer.new(minutes, ui)
init_plugins
end
def init_plugins
puts "init plugins"
@plugins = []
::Plugins.constants.each do |name|
@plugins << ::Plugins.const_get(name).new(self)
end
end
def start
timer.start
end
end
class StdioUi
def notify(text)
puts text
end
end
SleepTimer = Struct.new(:minutes, :notifier) do
def start
sleep minutes * 60
notifier.notify("Tea is ready!")
end
end
module Plugins
class Beep
def initialize(tea_clock)
tea_clock.ui.extend(UiWithBeep)
end
module UiWithBeep
def notify(*) #gets called by notifier.notify("Tea is ready")
puts "BEEP!"
super #calls notify in class StdioUi
end
end
end
end
t = TeaClock.new(0.01).start
The Book: I keep recommending this excellent book, Metaprogramming Ruby. I was consulting it while composing this answer.
So, here you extend an object with a module. In Ruby it’s called Object Extension. In simple cases it all works as expected, like this one:
Things get complicated when there are class’ own methods involved. Here’s a simplified version of your snippet that exhibits the same behaviour.
When you call a method in ruby, the interpreter has to first find a method to call. This is called Method Lookup. Now, when you define an instance method, in reality it’s a method on class object, not that instance. So, method lookup goes like this for first snippet:
When you extend an object, however, methods are injected into instance’s
eigenclass. It’s a special “hidden” class, unique for each instance. And in reality method lookup goes through it first. First snippet again:Now it should be clear why
Foo.hellois called instead ofBar.hello: because it appears earlier in the method lookup process!I may have made a few mistakes but this is roughly what happens.