SOLID Design pt. 1 — Liskov and Breakfast Foods
S.O.L.I.D is an acronym for the first five object-oriented design principles by Robert Martin. These principles make it easy for developers write clean, modular code that is easy to read and refactor. The acronym stands for Single-responsiblity principle…Open-closed principle…Liskov substitution principle…Interface segregation principle…Dependency Inversion Principle…And together they make your software SOLID.
For my first years of teaching myself development, I never grasped these principles in a replicable way. Intellectually sure, but in the text editor trenches, programs were 100s+ lines of code masterscripts, classes initialized their own dependents, and inheritance was a idealized dream of the future. For anyone currently living that life, here’s a (hopefully) helpful pointer in the right direction.
Object oriented conceptually lends itself to simple modeling of real world examples. For this demo, I will use the example of making a simple breakfast of two fried eggs.
Here’s the pseudocode of the process:
### The important steps:# Turn on stove
# Heat pan
# Acquire eggs
# Crack eggs into pan to cook
# Acquire plate
# Put eggs on plate
# Clean pan
# Turn off stove
# Eat eggs
# Clean plate
According to the Single Responsibility Principle, each class in the program should only do one thing. While the obvious example is that an egg shouldn’t both be an egg and cook itself, in practice, this is mostly subverted when object assumes it knows how its data is going to be used and reveals its data in inflexible way. Imagine if a cracked egg always came out with the yoke and whites mixed. The eggs is presenting its data (the edible portion) in a way that is undesirable to anyone not wanting scrambled eggs. Really the egg should not be presenting its data at all. It should only have accessible data and let the end user decide what to do with it.
Let’s look at the classes necessary for breakfast and make sure each process is encapsulated in one class:
# Stove
# - heats up cookware# Pan
# - holds contents
# - heats via conductivity
# - cooks contents
# - cleanable# Egg
# - is cooked# Plate
# - holds contents
# - cleanable
That’s pretty tidy. No class explicitly mentions another and class gives out its data rigidly (or at all.)
Now let’s take the verbs and make them methods on the classes. Note: An action by one class causes an effect on it’s recipient if there is one. The stove being hot causes the pan to heat up causes the egg to cook. Pseudocode of classes with pertinent methods:
## Stove
# - turn_on(heat level)
# - heat
# - turn off
# - contents=## Pan
# - heat_up
# - contents=
# - clean## Eggs
# - open
# - cook## Plate
# - contents=
# - clean
According to the Liskov Substitution Principle, modules that inherit from a parent must maintain the functionality of the parent. In this example, I notice Pan and Plate both share the functionality of clean
, also I know they are both a type of Dish. They will share that parent and enhance that parent’s functionality, but the real question is can both a Pan and Plate function as a Dish if needed? A Dish hold contents and is cleanable. So (and ask any college student), if in an emergency you desperately need any dish, a plate or a pan would suffice.
But how about a Stove? It, at bare minimum, can also hold contents and be cleaned. The missing piece here is incomplete pseudocode. Part of the unimportant bits I omitted are pick up plate
and pick up pan
. So a Dish also has an attribute portable
. While this is an obvious conclusion, most others will be less so, and the ability to reason through your decisions is a valuable skill to have when explaining your code to another person.
Check out Part 2 — Inverting Dependencies and Segregating Interfaces where I demo Interface Segregation Principle, Dependency Inversion Principle, and Open/Closed Principle.