If it quacks like a duck...
posted by andy, Wed Feb 20 09:00:00 UTC 2008
If it quacks like a duck...
duck-typing is the Way of Ruby and Rails programming. The phrase attributed to James Whitcomb Riley refers to the preference in Ruby and Rails to be concerned with what an object does (methods and attributes) rather than what an object is (it's class). That makes a lot of sense from a modeling perspective. We create classes as representations of like things, but we really deal with them in terms of the way we get to the distinguishing attributes and the kinds of things that it can do because it is a member of that group.
It makes even more sense from a Rails perspective. One of the great mantras of Rails programming is the DRY principle: Don't Repeat Yourself. A primary means of not repeating yourself is to capture reusable functionality in modules and mix those modules into the classes that need to be able to do those kinds of things. Modules are, first and foremost, abstractly written collections of methods... ways of describing how a duck quacks...
Duck typing is more than just an interface implementation. In most static languages the emphasis is on grabbing a particular interface on an object and then making use of that interface as a contract for interaction. With duck typing you really could not care less whether or not you have an interface. You really only care about whether or not the duck can quack. So you ask it:
duck.respond_to?(:quack)
How I learned to love ducks
Okay, so all the duck typing talk may be old hat to you by now. No matter. It was old hat to me, too. I just forgot to do it. Call it old habits dying hard but my code was littered with type-checking.
A particularly bad area of my largest application is designed to allow the end user to set up their own business processes. The code is full of subclasses that each perform one business function and the user hooks them up to design their larger processes. Where it begins to suffer is from the "all roads lead to Rome" problem. For our customers there is a central object with which they deal, but different users will typically get a handle on it through a different association. Rather than force the user to navigate up the object graph we decided to let the process step do the navigation for them. Falling back on old habits I'd have code looking like this
def apply(target_object, attributes={})
case
when target_object.is_a?(Widget)
return target_object.foo.bar! attributes
when target_object.is_a?(Cog)
return target_object.foo.bar! attributes
when target_object.is_a?(Foo)
return target_object.bar! attributes
end
raise(ArgumentError, "No, foo on you!")
end
The basic idea is to call the bar! method on a Foo class object, but allow the user to apply the process in any context that knows how to navigate up to the Foo class object (including a Foo instance itself). Obviously the repetition was screaming for some refactoring. Enter duck typing.
def apply(target_object, attributes)
foo = target_object.respond_to?(:foo) ? target_object.foo : target_object
raise(ArgumentError, "Duck foo on you") unless foo.respond_to?("bar!")
foo.bar! attributes
end
Duck typing brings a lot of advantages to that code. Obviously it eliminates the repetition. An important corollary is that it reduces the code base so there is less to maintain and less to break. Duck typing in this scenario also decouples the method from the classes that might need to use it. This is important in two regards. First, it means that I can eliminate one of those classes like the Widget class and not have to modify all the places that refer to the Widget class to eliminate 'undefined constant' errors. Second, it means that I can now introduce new objects that have a handle on the Foo class and automatically gain the ability to use this process step without modification. A less obvious point is that the code now fails more quickly if it's passed a target object that doesn't know Foo; it does not have to wait until all the branches of the case statement fail.
In my opinion, the nicest part of the duck-typing focused refactoring is that it makes the intentions of the method much clearer. Like a really good joke or a really bad pun, the punch is in the final line. What's the objective? To call 'bar!' with the appropriate attributes on a Foo instance.