Method and Message
Video transcript & code
mymethod = def hello puts "hello, world" end mymethod # => nil
However, we can ask for an object representing a methods. Here's a greeter class; we ask it for an object representing its
class Greeter def hello puts "hello, world" end end greeter = Greeter.new m = greeter.method(:hello) # => #<Method: Greeter#hello>
m.call # >> hello, world
But why should we even have this extra step? Why aren't Ruby methods first-class, anyway?
[su_note note_color="#eeeeee" radius="0"]Editor's note: Classes are first-class objects in Ruby. Methods are not.[/su_note]
The choice not to make methods first-class objects out of the box is fundamentally consistent with Ruby's nature as a purely object-oriented language, in the vein of Smalltalk. To understand why, we need to understand the difference between methods and messages.
Let's say we're writing a tea timer application. The top-level
TeaClock class has a
timer collaborator, which will handle the actual counting down, and a
ui collaborator, which will handle user interaction, including notifying the user of important events like their tea being ready.
class TeaClock attr_accessor :timer attr_accessor :ui #<<TeaClock-initialize-a>> #<<TeaClock-init_plugins>> #<<TeaClock-start>> end
timer role has the responsibility of waiting until the tea is ready and then triggering an alert. But it doesn't need to know or care how that alert is conveyed to the user. There might be different configurations of the app, one which notifies the user on the command line, another which pops up a growl-style desktop message.
In order to completely decouple the "timer finished" event from how it is shown to the user, we decide to simply inject a
notifier object into the
timer, which it will
#call when ready. Here, we have a simple
SleepTimer = Struct.new(:minutes, :notifier) do def start sleep minutes * 60 notifier.call("Tea is ready!") end end
That takes care of the
timer role. Now for the
ui role. Here's a really basic
UI implementation, which simply uses
STDOUT to communicate with the user.
class StdioUi def notify(text) puts text end end
On initialization, the
TeaClock class wires a
timer instance to a
ui instance. Remember, the
timer just expects a
call-able object. So
TeaClock turns the
#notify method into a
Method object and passes that to the
We also want our
TeaClock to be extensible, so once it's done wiring collaborators together, it initializes any user plugins that have been loaded.
def initialize(minutes) self.ui = StdioUi.new self.timer = SleepTimer.new(minutes, ui.method(:notify)) init_plugins end
To initialize plugins, it looks for any constants defined in the
Plugin module namespace, and passes itself to their constructors.
def init_plugins @plugins =  ::Plugins.constants.each do |name| @plugins << ::Plugins.const_get(name).new(self) end end
Here's a simple plugin. It dynamically extends the
ui object with a module that adds extra behavior to the
notify method. The idea behind this plugin is that it will augment the
StdioUi by also ringing the system bell when the time is up. For this demo, so that you can see the behavior in the screencast, we'll have it just print out the word "BEEP" instead of actually ringing the bell.
module Plugins class Beep def initialize(tea_clock) tea_clock.ui.extend(UiWithBeep) end module UiWithBeep def notify(*) puts "BEEP!" super end end end end
Finally, back on our
TeaClock class, we define a method to start the countdown, which simply delegates to the internal
def start timer.start end
OK, it's time to put all this together and try it out. We'll create a
TeaClock instance and tell it to start timing. So that we don't have to wait too long to see results, we'll pass it a very small unit of time.
t = TeaClock.new(0.01).start
…wait a second. That's not right. Where's the beep?
The problem lies with how we connected the timer with the UI. When we created a method object for the
# ... self.timer = SleepTimer.new(minutes, ui.method(:notify)) # ...
We passed in a handle to the specific implementation of the
#notify method at that point in time. And that point in time was before we initialized the plugins!
Now let's change the
TeaClock initialization code to simply pass the whole
ui object to the
timer, instead of a method object.
def initialize(minutes) self.ui = StdioUi.new self.timer = SleepTimer.new(minutes, ui) init_plugins end
That means we also need to change the
SleepTimer class to call
#notify on the
notifier, instead of
#call. Hold up a second. Did you hear what I just did?
I said "call
#notify". When what I really meant is that
SleepTimer should send the =#notify= message= to the
notifier. This is a great example of why the message/method divide is so hard to tease apart: conventional language around OO programming has become so dominated by the method-oriented terminology of languages like C++ and Java that we often use them interchangeably. I'll talk about the difference more in a moment, but for now just know that when I write "notifier.notify" in Ruby, that means I'm sending the =#notify= message to the =notifier= object.
SleepTimer = Struct.new(:minutes, :notifier) do def start sleep minutes * 60 notifier.notify("Tea is ready!") end end
class TeaClock attr_accessor :timer attr_accessor :ui #<<TeaClock-initialize-b>> #<<TeaClock-init_plugins>> #<<TeaClock-start>> end
Now let's try running our timer again.
t = TeaClock.new(0.01).start
Ah, that's better.
This code neatly demonstrates the difference between calling a method and sending a message. When our code saved a direct reference to a method and then called it later, it missed out on any changes to that method which occurred between when the reference was saved and when the method was executed. When we changed the code to send the
#notify message to the
notifer, it got the most up-to-date implementation of that responsibility.
To summarize the difference:
- A message is a name for a responsibility which an object may have.
- A method is a named, concrete piece of code that encodes one way a responsibility may be fulfilled. You might say that it is one method by which a message might be implemented.
You can look at messages as an extra layer of indirection on top of methods. When an object receives a message, it gets to decide how to respond to that message. How it responds to a message may change over the course of the object's lifetime. When code passes references to specific methods around, those references are in danger of going stale. It introduces a subtle temporal coupling between collaborating objects.
By sending messages to their collaborators, objects are assured of getting the current, correct behavior no matter what. The temporal coupling becomes a more benign name coupling.
Alan Kay envisioned objects as independent cells, passing messages to each other with no assumptions about how those messages would be handled. Ruby hews to this vision of object-oriented programming. So if you've ever wondered why we don't pass references to methods around more often in Ruby, now you know.