Depth and Direction
April 4, 2014
I’ve been reading a lot this week about the SOLID principles and various design patterns in object-oriented programming, and find myself often thinking about the concepts of depth and direction. Understanding “where” different classes exist and the directions of their dependencies is crucial to designing applications cleanly.
When first learning object-oriented programming, it’s easy for beginners to get caught up in naming and modeling classes after real-world objects. My early projects in Rails frequently demonstrated this approach–I understood models as “the place where all the methods go.” Uncle Bob has a great example of this tendency using a coffee-maker exercise. The gut instinct is to think of all the “nouns” in the system (CoffeeMaker, WaterHeater, CoffeePot, etc.), turn them into classes, and tie them all together. The problem with this approach is that it leads to what Uncle Bob calls “Vapor classes” and “God classes”. The former seem to do something or another, but in fact are empty vessels that provide little more than a seemingly-convenient naming wrapper. For example, it may seem neat to beginners to be able to turn on a light by calling
Light.on, but if all that method really does is call
CoffeeMakerAPI.api.setIndicatorState(CoffeeMakerAPI.INDICATOR_ON), there really isn’t anything substantial going on in that class.
In applications with this kind of design, it isn’t hard to find where things actually happen. There’s probably one class named after the system being built (ex. CoffeeMaker) that is a large, unwieldy collection of all sorts of methods. This is the God class, and is obviously a violation of the Single Responsibility Principle. Unfortunately, the God class is a very easy trap to fall into (especially in Rails). I think part of what makes this anti-pattern so attractive to beginners is that it can seem hard to understand how the classes in a better-designed system will interact with one another. It seems like there must be some class responsible for “putting everything in order”, and that this class is, naturally, the CoffeeMaker itself…
Abstractions and Implementations
…when instead, I’m finding it better to think of the entire system, the application itself, as the CoffeeMaker! And the application is not a collection of nouns, but a collection of verbs–of behavior.
As Uncle Bob demonstrates, a better approach is to first think of abstractions based on behavior. After all, the reason we’re writing software in the first place is to do things. I really like Uncle Bob’s phrasing, so I’m going to quote it directly: “It is the behavior of a system that is the first clue to how the software should be partitioned.” That word partition is important. The beginner approach I described above partitions classes based on concrete, real-world nouns. This might work for a hardware engineer, since he/she has to make physical things, but in software, we are dealing with more abstract systems.
Uncle Bob begins his example with a HotWaterSource, a ContainmentVessel, and a UserInterface. Using these three classes, we can describe at a “high level” the process of making coffee. Each of these classes has very general behavior, such as “indicates readiness”, “starts boiling water”, and “pauses process”. Once we understand how these high level classes interact with each other, we can build “low level” classes to implement the gritty details like
getWarmerPlateStatus(). Per the Dependency Inversion Principle, the low level classes depend on the high level classes, not vice versa. In other words, the higher, abstracted classes can function and interact with each other properly without needing to know about the details of how those interactions are actually occurring.
Patterns and Advantages
Several advantageous OO design patterns emerge surprisingly organically when building applications in this manner. For example, my Tic Tac Toe game uses the Strategy Pattern with regards to player moves–switching between a human player, an easy computer (that just takes the first available spot on the board), and an unbeatable computer (that uses the MiniMax algorithm to never lose) is trivial. The game talks directly to a relatively abstract “Player” object, which has a
decision_maker attribute. The decision maker can be the ConsoleUI (for human players), the SimpleAI module, or the UnbeatableAI module, each of which has its own way of determining the next spot on the board to play. I implemented the Strategy Pattern naturally without even knowing it was a tried and true OO design pattern.
In Uncle Bob’s coffee maker example, we have a high level system in place that broadly describes the process of making coffee, and we have low level details pertaining to a specific coffee maker. The structure of the application allows us to reuse it for a different brand or model of coffee maker–we just need to make new low level classes for the specifics of how the new brand/model works, and build them in such a way that they depend on the high level abstractions so that they can be wired in without needing to change anything above.
In my meetings with Rylan, while working on my bank, Tic Tac Toe, or other projects, we’ve often talked about “pushing the details down.” It’s not the easiest concept to grasp when starting out, and frankly I find many diagrams illustrating class relationships to be decidedly unhelpful (I need to build some kind of 3D diagram system…). But by studying the SOLID principles, keeping them in mind, and working on projects with my mentor, I feel I’m developing an improved sense for talking about and executing clean design and architecture.