{ |one, step, back| } 1 of 1 article Syndicate: full/short

Thinking in Ruby   05 Jul 05
[ print link all ]

A language that doesn’t affect the way you think about programming, is not worth knowing.—Alan Perlis

Alan Perlis, the first head of the Carnegie Mellon University Computer Science Department and the first recipient of the Turing Award, recorded some of his accumlated knowledge about programming in a series of one sentence statements. You can read more about his Epigrams here.

I especially like number 19, about learning programming languages (quoted above). That’s the big reason the Pragmatic Programmers recommend learning a new language every year. To expand the way you think.

Thinking Different

Here’s an example of one way that Ruby affects the way you can approach a problem. I was preparing for my Dependency Injection talk (to be given as OSCON) and was working out the details of an example program I was using to demonstrate some DI features. I chose the “Mark IV Coffee Maker” problem from “Robert Martin:http://objectmentor.com (used with permission). I love the coffee maker problem and have used in C++ and Java courses over the years. It is simple enough to grasp in a teaching scenario, but rich enough in objects to be interesting. And because it is a physical control system, a lot of the objects are simple analogs to physical devices and therefore easy for beginning OO designers to work with.

I’ve written code for this example, oh, probably half a dozen times over the years; sometimes in C++, sometimes in Java (and once in Eiffel if I recall correctly). The solution changes with each iteration, but there are some common themes that keep appearing.

Java Coffee

One small part of the problem deals with a Brewer object that needs to control a Heater object (which implements an On/Off device interface) and a relief valve (which conforms to an open/close device interface). This is an excellent place to introduce two design patterns to the students. The first is the composite pattern where we create a Boiler object that is a composite of both the Heater and Relief valve. The Boiler becomes a single object to the brewer which just issues on/off commands to the boiler. The boiler is responsible for making sure all its subcomponents (the heater and the relief valve) are properly operated.

The composite pattern works by allowing the composite to forward the on/off commands to each of its subcomponents, but the valve wants to recieve open/close commands, not on/off commands. Here is the second pattern that emerges from this portion of the design: the Adapter Pattern. By wrapping the relief valve in an on/off adapter, the boiler composite sees a unified on/off interface that is translated for the valve to an open/close interface.

For the Dependency Injection talk, I was developing both a Java verison and a Ruby version in parallel. The Java version was straight forward and I implemented it like I normally do. The only big design choice is how to map on/off commands to open/close. For the coffee maker, on needs to map to close and “off” needs to map to “open” (when the boiler is off, the relief valve needs to be open to relieve the steam pressure).

The opposite mapping might make sense in other problems, and you alway think about making classes like the OnOffAdapter a bit more flexible so that it can be used in the other situations. I considered adding a flag to the adapter constructor to allow the user to select the desired mapping, but decided against it. I didn’t need it for this problem and felt the additional logic would just get in the way.

The Ruby Adapter

So now it was time to write the Ruby version. The straight forward implementation of of the Ruby adapter is simply:

  class OnOffAdapter
    def initialize(device)
      @device = device
    end
    def on
      @device.close
    end
    def off
      @device.open
    end
  end

Once written, I again considered adding a flag, but suddenly realized there was a better solution:

  class OnOffAdapter
    def initialize(device, on_command, off_command)
      @device = device
      @on_command = on_command
      @off_command = off_command
    end
    def on
      @device.__send__(@on_command)
    end
    def off
      @device.__send__(@off_command)
    end
  end

Instead of a flag that would only switch between two different on/off and open/close mappings, by adding the commands themselves to the interface I get an adapter that will adapter any device to an on/off interface (well, almost any device … keep reading).

To use the device in my coffee maker example, all I need is:

   valve_adapter = OnOffAdapter.new(relief_valve, :close, :open)

Once I took the step of making the open/close commands generic, I realized that I could make the on/off generic as well.

  class Adapter
    def initialize(device, mapping={})
      @device = device
      @mapping = mapping
    end
    def method_missing(sym, *args, &block)
      command = @mapping[sym]
      if command.nil?
        super
      else
        @device.__send__(command, *args, &block)
      end
    end
  end

Usage is now:

  valve_adapter = Adapter.new(relief_valve,
    :on  => :close,
    :off => :open)

I now have a generic adapter that will do a one-to-one mapping from one set of methods to another set of methods.

Continued Refinement

But is doesn’t have to stop there. As an exercise for the student, consider what it would take to add the following refinments:

  • Allow mappings that translated on and off commands to something like brightness_level(10) and brightness_level(3). (Hint: think about procs).
  • Allowed methods that were not mapped to pass through unchanged.
  • The current version creates an object that does the adaptation. If you need a bunch of similar adapters, would it be better to return a class whose instances are adapters implementing the defined mapping? How would one implement that.

Beyond Java

The point of this posting is not to bash on Java. Certainly you can create generic adapters in Java that can do much the same mappings as the Ruby version. But getting from the specific to the generic adapter is a big step, big enough that my YAGNI filtered kicked in and I didn’t go down that path in Java.

In Ruby, the step was small enough that refining the adapter was very easy and natural. And the end result yielded some worthwhile insites into the adapter pattern as well.


blog comments powered by Disqus

 

Formatted: 09-Feb-12 20:48
Feedback: jim@weirichhouse.org