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:
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
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.