After spending the last few months becoming well-grounded in Ruby, this week I am in the middle of reading a book that promises to change your life. No, it’s not the Bible. Or The 7 Habits of Highly Effective People. Or The Brothers Karamazov (I have previous with that one). Or even Mastering the Art of French Cooking. I’m talking about Sandi Metz’s Practical Object-Oriented Design in Ruby. Am I being hyperbolic? Not according to the book’s author, who tells me very clearly on page 67:
[The] transition from class-based design to message-based design is a turning point in your design career [as] the message-based perspective yields more flexible applications.
And you know what? I think I believe her. Her insights on object-oriented (OO) design have caused me to fundamentally rethink how I approach programming problems. As a means of documenting this theoretically seminal week in my professional development as a programmer, this post will examine a couple key OO principles that Metz has introduced to me.
Won’t Somebody Please Think of the Children?
Perhaps the most succinct expression of one core tenet of object-oriented design comes not from Metz herself, but from Obie Fernandez, who writes in the book’s foreword:
In almost all cases, maintainability over the life of the code is more important than optimizing its present state.
This makes a lot of sense, both from a design standpoint as well as from a financial standpoint. The world is a dynamic place and never stands still, and the tech world all the more so. Any code that’s written today will surely require changes tomorrow in order to adapt to new conditions. If there were a way to structure a program where which it would be possible to make these changes quickly and efficiently in two places instead of needing to search through lines and lines of code to locate 25 potential breaking points, shouldn’t this be done?
Concentrating on safeguarding the future of the code instead of writing the most efficient procedures for the small task directly in front of me represented a seismic shift in how I conceptualize a programmer’s job. This is the equivalent to a spendthrift realizing for the first time that the purpose of money isn’t to help maximize one’s pleasure in the present moment, but rather to secure oneself (and, yes, one’s children) for future eventualities.
Even when I look through the relatively simple Bingo code I presented in last week’s post, I see places where better organization and design would make it more flexible and make future changes easier to implement. But rather than delve into the minute details here, let’s continue to focus on the macro level—that is, the global principles of OO design.
Managing Your Dependents
While it is of course impossible to fully future proof your code, if you want to greatly increase the chances it will be viable in the future, nothing is more important than managing its dependencies. What exactly does this mean from a programming standpoint? Basically, whenever one class (let’s call it “class A” for the purposes of this discussion) contains explicit references to another class, another class’s behavior, or some other concrete characteristics of another class, it becomes dependent on it. That means that any change made to the other class has the potential to create a ripple effect and ruin the functionality of class A.
Therefore, the more dependencies you create between class A and other classes, the more fragile your design becomes as everything becomes interconnected and mutually dependent, as in a house of cards (displayed to the right for those who like visual metaphors). One small change and everything can come tumbling down.
On the other hand, it’s well nigh impossible to write a useful program where different classes and different objects are unable to “talk” to each other. Creating links between classes is inevitable, otherwise nothing would get accomplished. So, what’s the solution? The trick is to design the classes and their interconnectivity in such a fashion as to minimize or even avoid dependencies. Communication between classes that does occur should go along clear, predictable pathways. (The visual metaphor I’m going to reference this time is the bonds of a molecule, where each atom can represent a class or instance of an object.) There’s definitely an art to doing this, and Metz presents a gamut of refined techniques to remove or neutralize dependencies between classes.
Closely intertwined with managing inter-class dependencies and paths of communication is yet another foundational principle of object-oriented design theory.
Master of One
The quaint English phrase “Jack of all trades, master of none” would never be used to describe a properly constructed Ruby class. Classes should excel at doing one thing and one thing only. Designing classes in this fashion increases their modularity and, perhaps even more importantly, helps prevent the formation of dangerous dependencies between classes. Metz writes that classes that function well together should say to each other,
“I know what I want and I trust you to do your part”
while further noting that:
This blind trust is a keystone of object-oriented design. It allows objects to collaborate without binding themselves to context and is necessary in any application that expects to grow and change.
Let’s imagine that we have a person who belongs to a class we’ll call
Customer. This person knows exactly where he wants to go—to sunny California on vacation. He needs to take a taxi to the airport and then board a plane that’s flying to San Francisco. If we were to design this
Customer class, it would be perfectly acceptable to have a
#buy_ticket method, a
#hail_taxi method, and a
#board_plane method. However, the customer, even though he knows where he wants to go, should not grab the wheel of the car or bust into the cockpit to take over the plane’s controls. These are the responsibilites of the
Pilot classes, respectively.
Customer is trusting other people to help him complete the procedure of getting from A to B. Should unforeseen obstacles crop up that prevent the
Customer from precisely following the meticulous procedure he had planned, he can send messages to other knowledgable people, masters of one, that will try to help him find a solution. Whereas if the
Customer had only himself to rely upon and the procedure somehow got off track (snow at O’Hare?), he would have to fundamentally recalculate his entire plan and come up with a new procedure (that is, if he even possessed the capabilities to do this himself).
Okay, admittedly that metaphor was a little strained, but I think it nonetheless elucidates some of the core tenets of object-oriented design (planning for future eventualities, avoiding explicitly stated dependencies on any one condition or structure, and relying on experts (other classes) to do their jobs well. It also demonstrates (again, in a slightly strained fashion) the difference between object-oriented programming and its opposite, functional programming. By following the principles of object-oriented design, software becomes more flexible, more adaptable, more modular, and more amenable to future changes. Mastering these concepts allows a programmer to not only build programs for today, but for tomorrow as well. Onwards and upwards, fellow Rubyists!