Having recently graduated from the Flatiron School, I am working my way through programming-related reading, beginning with Sandi Metz’s “Practical Object-Oriented Design in Ruby”. Click here for more posts about POODR.
My hope for this blog post is not to intimidate (as Sandi Metz suggests below), but to communicate the concepts of polymorphism and duck typing in Ruby, as Metz describes them in Chapter 5 of POODR.
The term ‘polymorphism’ is commonly used in object-oriented programming
but its use in everyday speech is rare enough to warrant a definition. Polymorphism expresses a very specific concept and can be used,
depending on your inclinations, either to communicate or to intimidate.
Either way, it’s important to have a clear understanding of its meaning. First, a general definition: ‘Morph’ is the Greek word for form, ‘morphism’ is the state of having a form, and ‘polymorphism’ is the state of having many forms. Biologists use this word. Darwin’s famous finches are polymorphic; a single species has many forms. […] Polymorphism in OOP refers to the ability of many different objects to respond to the same message. Senders of the message need not care about the class of the receiver; receivers supply their own specific version of the behavior.
(Bolded emphases mine)
A pretty simple definition for a somewhat complicated word. Polymorphism, the state of having many forms. Specifically, for our purposes in object-oriented programming, the ability of many different objects to respond to the same message. Let’s flesh this out.
An example: Metz’s Bike Co. (i.e., what problems does duck typing solve?)
Imagine a bicycle touring company looking to automate its process of booking trips. Each trip needs to be prepared by a mechanic (ensuring the bicycles are in working order), a trip coordinator (handling logistics, like food, for the trip), and a driver (ensuring the vehicle is ready to transport the trip participants). The code below (straight from POODR) is an example of how not to do this. But why?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
I’ll let Metz explain what’s wrong with this:
Count the number of new dependencies in the prepare method. It relies on specific classes, no others will do. It relies on the explicit names of those classes. It knows the names of the messages that each class understands, along with the arguments that those messages require. All of this knowledge increases risk; many distant changes will now have side effects on this code.
To make matters worse, this style of code propagates itself. When another new trip preparer appears, you, or the next person down the programming line, will add a new when branch to the case statement. Your application will accrue more and more methods like this, where the method knows many class names and sends a specific message based on class. The logical endpoint of this programming style is a stiff and inflexible application, where it eventually becomes easier to rewrite everything than to change anything.
A lot of negative consequences: class dependencies, risk of side effects for distant changes, propogation of this type of code, and ultimately a rigid application that requires a rewrite. This is where duck typing comes in. It can prevent all of those things.
Solving these problems with duck typing (i.e., what is duck typing anyway?
Duck typing is about defining objects by what they do instead of who they are. Think about Trip’s
prepare method—what does it do? Metz describes, “the method serves a single purpose, its arguments arrive wishing to collaborate to accomplish a single goal. Every argument is here for the same reason and that reason is unrelated to the argument’s underlying class” (90). It’s not about the class of each argument, be it
Driver. If it walks like a duck and quacks like a duck, treat it like a duck (or, in this case, like a
Preparer). Consider the example below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
prepare method and each other class’ unique preparation methods (they’ve all become
prepare_trip). We’ve effectively implemented a
Preparer class, but it has no concrete existence; it exists only as a duck type. Now all objects that implement
Is this better? How? First of all, there’s a certain symmetry and conciseness to it. We can all agree (or should, anyway) that’s better. Second, though our first example was more concrete (and, thus, perhaps simpler to understand), we now have more extensible code. It’s slightly more abstract, but changes to the code require us only to turn another object into a
Preparer, rather than changing our original
prepare method to include yet another class.
Understanding these tradeoffs can be difficult, and it boils down to a tension between concretion and abstraction. Metz explains that “Concrete code is easy to understand but costly to extend. Abstract code may initially seem more obscure but, once understood, is far easier to change. Use of a duck type moves your code along the scale from more concrete to more abstract, making the code easier to extend but casting a veil over the underlying class of the duck” (94).
This is all great, but how can we recognize ducks (and thus duck-type our objects) if it is, in fact, a difficult situation to recognize? Three common patterns can be replaced with ducks: (1) case statements that switch on class (this is what we saw in the original bad example), (2)
is_a?, and (3)
responds_to?. Each of these patterns deals with the object’s behavior. If you find yourself using these patterns to check on an object’s behavior, remember: if it behaves like a duck, treat it as such.
Why did I introduce this post with a definition of polymorphism? Because duck typing is a reality of OOP only because OOP is polymorphic. Though duck typing is only one way to achieve polymorphism in Ruby (inheritance and behavior sharing with modules are others), it is a great example of it. Duck types leverage the ability of many Ruby objects to respond to the same message, which is the very definition of polymorphism.