Software Design is hard. It is complicated, it takes years to learn (in a sense that you really know what you are doing and why, compared to just copying and pasting something that you read somewhere) and sometimes it is counter-intuitive.
I started making these notes to better understand SOLID design principles myself while reading the “Clean Architecture” by Robert C. Martin. The book points to common misunderstandings of these principles, but the explanations given are far from being easily memorable.
To make Software Design a bit more entertaining, I decided to write a short funny poem for each of the five SOLID principles, complemented with examples from real life that ideally even non-engineers can understand.
Solid Responsibility Principle (SRP)
A servant serves a single stew To eaten be by only you. Whenever master's mind is changed, The stew is promptly rearranged.
This principle is usually (mis)understood as “A class should only do one thing” or even “A class should only have one public method”. But the actual definition is:
A module should be responsible to one, and only one, actor.
The difference is easy to illustrate with an example. Consider a software model of a small
DigitalAgency. It has 3 departments:
TechDepartment. And there is one
Printer that serves all the needs in this small company: Finance employees print their reports, graphical Designers print previews of their work, and the Tech geeks print their online bookings, xkcd comics and what not:
So far so good: the
Printer has only one method
print() which does something relatively simple (i.e. just prints whatever it’s sent). But there are three actors depending on it (no matter if they inherit it, call it directly or call it via shared
interface). Thus, it violates SRP. Indeed, let’s say that the
DesignDepartment is doing well in graphical design and now they need a much better
Printer to print full-color A0-sized posters on glossy paper. What do you do? Add another method
printA0() or make the
print() method print full-color A0s by default?
TechDepartment is amused by the idea of using that printer to make wall-sized comics! But
FinanceDepartment is angry, because the geeks would be wasting the ink, the time of everybody waiting in the printer queue, and of course company $$$s. Thus, the obvious solution is to have not 1 but 3 different Printers, each one fitted for the needs its actor:
That brings a temptation of explaining SRP as “Each class should have only one class dependent on it”, but in reality this would mean tons of merely duplicated code:
To deduplicate our effort we could make a reasonable violation to the SRP principle. For example,
TechDepartment could still use printers of the same cheap model, while
DesignDepartment would be using its shiny
In simple words, the SRP principle can be rephrased as:
A component reports to only one boss.
Open-Closed Principle (OCP)
The book is open, but it's closed. It's read, adopted and exposed, But to add more ideas Without legal fears, A new one has to be composed.
The original definition reads as:
A software artifact should be open for extension but closed for modification.
As an example, we’ll use a famous electronics company which has been producing a very successful model of a
VacuumCleaner for many years. Its
VacuumCleanerBase has a powerful and reliable
Engine, all its components have been perfectly fit together and proved over time. The time though keeps going by and the governments come up with energy consumption regulations, so the mighty but fixed
1800 Watt power characteristic is no longer an advantage. What does the company do? Invent a new model, spending lots of money for research, engineering, testing, etc.? Modify the internals of
VacuumCleanerBase so that it is no longer well-fit and reliable? No, there’s a better way. The old good
VacuumCleanerBase can be extended using a simple
PowerInvertor device that will allow a user to adjust the output wattage using a simple control knob or slider:
Of course, in order to support such an extension,
VacuumCleanerBase should already provide all the necessary interfaces. This kind of extensibility is one of the requirements of OCP.
Now, the company’s Marketing department makes a research and finds out that there is a huge demand on
AntiAllergicVacuumCleaner model from people having pets, rugs or living in dusty environment. Here is what the Engineering department thinks of it: easy! All they need to do is to design a
TurboBrush for the rugs and replace standard filters with a high class
For an electronics company this approach saves millions of dollars on engineering, testing and modifying production lines and supply chains. In software engineering, it can save quite some human hours at least.
Rephrasing the principle:
A component should be so simple that it rarely needs to be modified, but it should be easily extensible by other components.
Liskov Substitution Principle (LSP)
A founder founded a foundation Of large software corporation. Foundation was so strong and hard, It found a founder's counterpart.
The original definition is:
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
This definition is cryptographically scientific, but the easier the example we need. Let’s pick a slightly nostalgic one, with phone chargers and data cables.
Not so long ago, a hardware designer who was developing, say, a new Nokia or Sony Ericsson cell phone, would fit it with an A/C port, that a customer can plug a charger in. In the beginning, each model had its own type of charger. Then engineers (or was it marketing?) realized, that they can actually unify the chargers, so that they are interchangeable for all the cell phones produced by the company. Your Nokia is running flat but your colleague has a Nokia too? You can borrow their charger, what can be more convenient:
In this case your colleague’s charger is the substitute of the charger that you left at home. But then the principle got further: before you used a separate charger port and data cable port, but then some smart people came to use the same micro-USB for charging the phone and sending the data in and out. Just mounting the port on a device is not enough, the engineers had to ensure that all the peers would follow the same protocol, i.e. use the same interface. An absolutely wonderful consequence of following the LSP principle further this way was the fact, that now lots of phones of different brands could use the same chargers and the same data exchange software:
This diagram is simplified to encapsulate more components and implementation details, but what’s important is that the combination of the following interfaces:
- micro-USB Port
- micro-USB Cable
- MTP Data transfer drivers
makes it possible that we can interchange any phones, any cables and any computers that implement these interfaces and they will just work together.
Were there exceptions? Yes, for instance Apple devices kept using proprietary chargers. And even nowadays some devices, despite using the common micro-USB or USB-C ports, require brand-specific software instead of using the common protocols, giving us a good example of LSP violation. With violation of LSP comes the limitation of possible use cases, extensibility and convenience.
Back to software engineering theory, the principle can be rephrased as:
Use interfaces to make the dependency components interchangeable.
Note: the original definition doesn’t say anything about interfaces. It sounds more like using parent classes instead of child classes. That is so C++. This article targets slightly different languages and encourages using interfaces and aggregation over inheritance.
Interface Segregation Principle (ISP)
Man proposed to a princess is no fool: He's given a horse and a kingdom to rule. But there's a dragon to slay, And an army to pay, While all he wants is a girl and a mule.
A quote from Robert Martin’s book can serve as a definition:
it is harmful to depend on modules that contain more than you need
Although the name of this principle sounds somewhat chemistry, we can fall back to a simple example that you may have experienced in real life. Imagine that you just moved to a new home and you demand on broadband
Internet access. There are several Internet Service Providers that can connect your apartment, but only one of them can do it right away while with the others you have to wait for several weeks.
That provider though only offers ADSL
Internet that comes with a
Landline and a
MobileSIM package, all paid on a monthly basis with a minimal contract of 2 years. You don’t want to wait several weeks and decide that having a landline and a relatively cheap SIM is not such a bad thing. So, you sign the contract and start to depend on the following congregated interface:
This diagram shows just a few of the functions that you start to depend on as a daily routine, provided from an unsegregated
interface Internet provider.
After the first month you come to a conclusion that even though the ADSL is more or less OK, the calls on that SIM are pricey and you don’t need the landline at all. Altogether you’ve signed to pay twice as much for the services that you don’t need and there are way better deals on the market if you got a broadband and a SIM card separately:
In the latter case if the ADSL line goes nuts, you can change it for a better provider, without having to change your mobile SIMs as well.
So, what this principle recommends us is:
Don’t depend on things that you need and that you don’t need altogether. It’s better if they break separately rather than all at once.
Dependency Inversion Principle (DIP)
There lived a tail who waged a dog. It could not stay like that for long. They went to lawyer to conclude Their independent neighborhood. And now there's nothing left to nag: They both have documents to wag.
As stated by Robert C. Martin:
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.
This principle is the most difficult one to explain and illustrate to somebody who hasn’t spent countless hours building software. Dependency what? Inversion how? For sake of who?
But thinking of it, this principle is widely involved in how we learn and interact with the outer world: we interact with concrete objects via abstract interfaces. And these interfaces serve as protocols, that allow the dependency flow to be the inverse of the control flow, at least in the critical parts.
For example, say that you need a plumber to fix a leak. Alright, you know your friend George is good at things and probably can do it for you. Just ask George then. But what if George is not around or he doesn’t know how to fix a leaking tap in particular? Then you need an abstraction: a Plumber. And you need another abstraction to get you that Plumber in first place: some Household Service or even just Classifieds (both are just Abstract Factories in this case).
Let’s zoom in and generalize this example a little bit. So, consider we have a
Customer who relies on plumber
It’s good to know there’s
George to fix a leaky pipe. But what happens if
George is not there when a
Customer needs him? Or if the problem a
Customer has later on requires skills or equipment
George simply doesn’t have. It’s not so good for
George either if the only way he gets customers is via direct reach. Thus, this direct dependency may be effective in short term but not flexible over time.
With Dependency Inversion Principle applied, the diagram would look something like this:
In this case, a customer called
Bill implements an abstract interface
Customer and has abstract knowledge on how to get help from a generic
HouseholdService, e.g. when he needs a
Plumber. The interface
Plumber represents what a
Customer thinks a plumber should do. All the rest is behind the scenes: a double curved line also known as architectural boundary. For example, there can be a handy website
FindAPlumberDotCom that helped
Bill to find a plumber
George within seconds without any hassle.
So, where is the inversion? When
Bill has a leaky pipe, he still calls
George to fix it in the end - this is the control flow. But
Bill doesn’t depend on
George, he depends on an abstract interface that
George also depends on. A dependency from
Plumber interface is inverse to the control flow. This is why the principle is called Dependency Inversion.
Back in Software Engineering world, DIP means applying abstractions to implement achitectural boundaries and make the dependency flow in the direction stated by the principle: from concrete details to stable abstractions.