In which we meet a micro-pattern from the classic book Smalltalk Best Practice Patterns by Kent Beck, and see how it enables us to conveniently plug objects together.
In Episode 11, we looked at the difference between sending a message and calling a method. We started out with an SleepTimer
class that was initialized with a reference to a method on a "notifier" object. Once the given number of minutes elapsed, it would call the method in order to notify the user.
# Calling a method
SleepTimer = Struct.new(:minutes, :notifier) do
def start
sleep minutes * 60
notifier.call("Tea is ready!")
end
end
# ...
SleepTimer.new(minutes, ui.method(:notify))
We then changed it to a version which was initialized with a reference to the notifier object itself, and sent the #notify
message.
# Sending a message
SleepTimer = Struct.new(:minutes, :notifier) do
def start
sleep minutes * 60
notifier.notify("Tea is ready!")
end
end
# ...
SleepTimer.new(minutes, ui)
We realized that, somewhat counter-intuitively, the version where just the method was passed in actually introduced a tighter coupling than the message-sending version, because it bound the SleepTimer
to the notifier's implementation at a particular point in time.
The message-sending version is coupled to the notifier
collaborator by connascence of name. This is a fairly benign form of coupling. It's easy to understand, and easy to track down and change with global search and replace.
Nonetheless, sometimes we would like an extra level of indirection. For instance, it occurs to us that the $stdout
object would make a perfectly good notifier
, if only it supported the #notify
message. Since it doesn't, we had to wrap it in another class in order to adapt it to the needed interface:
class StdioUi
def notify(text)
puts text
end
end
What if, when initializing the SleepTimer
, we could tell it not only what object to use as a notifier
, but also how to use it as a notifier
? To make this possible, we apply the Pluggable Selector pattern described in Smalltalk Best Practice Patterns, by Kent Beck. We add a :notify_message
attribute to the timer class. Then we have it send that message to the notifier
.
# Sending a message
SleepTimer = Struct.new(:minutes, :notifier, :notify_message) do
def start
sleep minutes * 60
notifier.send(notify_message, "Tea is ready!")
end
end
Now we can use the $stdout
object as a notifier by itself, by also providing the selector, or message, as a symbol:
SleepTimer.new(minutes, $stdout, :puts)
There's one small improvement we can make to this code. We used the #send
method to send a message to the notifier. But the #send
method is a somewhat dangerous method, in that it will trigger a call to any method, even private ones. This could result in our accidentally tying our timer to private implementation details of another class. It would be better to use #public_send
, which respects object privacy boundaries and won't allow a private method to be called.
# Sending a message
SleepTimer = Struct.new(:minutes, :notifier, :notify_message) do
def start
sleep minutes * 60
notifier.public_send(notify_message, "Tea is ready!")
end
end
Here's another example of Pluggable Selector. ProductListPresenter
is a class which is responsible for listing out products in a web storefront. By making the message for showing a product pluggable, we are able to easily re-use the same presenter for concise receipt-style lists, and for more verbose menu-style lists that show the full product name.
Product = Struct.new(:short_name, :long_name)
products = [
Product.new("JonGldApl", "Jonagold apples from our own orchard"),
Product.new("PchTrnvr", "Fresh-baked peach turnovers"),
Product.new("TrkBrs", "Turkey bruschetta panini")
]
ProductListPresenter = Struct.new(:products, :show_message) do
def render
"".tap do |s|
s << "<ul class='product_list'>n"
products.each do |product|
s << "<li>#{product.public_send(show_message)}</li>n"
end
s << "</ul>n"
end
end
end
puts ProductListPresenter.new(products, :short_name).render
# >> <ul class='product_list'>
# >> <li>JonGldApl</li>
# >> <li>PchTrnvr</li>
# >> <li>TrkBrs</li>
# >> </ul>
puts ProductListPresenter.new(products, :long_name).render
# >> <ul class='product_list'>
# >> <li>Jonagold apples from our own orchard</li>
# >> <li>Fresh-baked peach turnovers</li>
# >> <li>Turkey bruschetta panini</li>
# >> </ul>
There's an old saying that you can fix any problem with another layer of indirection, except for the problem of too much indirection. Pluggable Selector adds a layer of indirection which, in most cases, would be overkill. But it's occasionally a useful tool for loosening the coupling between collaborators or for doing impedance-matching between codebases.
That's it for today. Happy hacking!
Responses