In Progress
Unit 1, Lesson 1
In Progress

Pluggable Selector

Video transcript & code

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