SOLIDifying: A Matter of Principle
Over the past few weeks, I’ve come across cases of engineers who, in my eyes, were committing flagrant transgressions of the elemental principles of programming. Seeing myself reviewing code while shaking my head, crossing things out with a red pen here and there like a grumpy schoolteacher, I had to ask myself if I wasn't turning into the old man yelling at clouds. After a brief period of personal soul-searching and profound life reflection, I concluded what anyone who has to live with themselves would: I am right, and everyone else is wrong.
And I'm right not just because the gentleman staring back at me from the other side of the mirror says so, but because I've spent years reading books written by very smart people that essentially boil down to: «Mario, you're right»—and whatever is written in books is 100% real, no fake; just ask Mr. Joseph Smith. But if I'm right—which I am—the scenario is even more unsettling, because it would mean that the people around me either suffer from collective amnesia or lied just a smidge.
Allow me to explain.
I remember the golden age of tech recruiting, that era when developers were auctioned off like fresh catch at the fish market. Recruiters paraded them around and companies threw fistfuls of cash. All a candidate needed were the right buzzwords: DDD, TDD, CI/CD, DRY, KISS, YAGNI, BSR, BDSM, SOLID... They were like the arcane words that opened the Doors of Moria: "Speak, SOLID, and enter," followed by a welcome pack with a thermal bottle, a t-shirt, a laptop, and off to live the DEV life.
Could it be that they never actually practiced the SOLID principles? There are two sacred things in this life: your Tinder profile and your LinkedIn profile, and nobody would ever exaggerate on either of them. So, giving my colleagues the benefit of the doubt, I'm going to assume the logical option: I've watched enough Stephen King movies to validate the collective amnesia theory (one—one movie is enough). Thus, like any self-respecting middle-aged gentleman, I shall proceed to evangelize with an unsolicited reminder, for whoever might need it.
To begin with, what are the SOLID principles? Let me tell you what they certainly are not: they are not laws etched in stone by a higher authority, nor are they metrics to evaluate code quality. They are, however, two things: a trump card to shut down arguments and justify a change—«because it violates the Single Responsibility Principle» (violating principles sounds terribly grave)—and a handful of heuristics for making sound decisions when in doubt.
First Heuristic: Single Responsibility
Imagine you're building a house. You get the requirements for the kitchen: it’s a room for working with water, it needs faucets, drains, and tiled walls for easy cleaning and drying. Then another requirement comes in: the bathroom. You realize it looks quite a lot like the kitchen, given that it’s a room for working with water, needs faucets, drains, and tiled walls... Why waste time and resources building two separate rooms? All in one: you slap a toilet next to the kitchen sink and call it a day.
Now then, turns out the French arrive with revolutionary ideas: liberty, equality, fraternity, and intimate hygiene. You have an additional requirement for your kitchen: something to wash your backside with soap and water (pardon my French). You have two options: sitting in the sink—something that won't thrill whoever has to wash vegetables there later—or squeezing a bidet right next to the fridge.
You decide to compromise: the fridge door can now only open halfway, but you figure it’s still worth it. What happens next will shock you: a new requirement. Changing times dictate that people shouldn't just clean their private parts; now the entire body must be washed. So you have to wedge a shower stall into your already cramped kitchen, complete with a glass door, towel rack, etc.
The takeaway is that even though they look similar and share characteristics, the kitchen and the bathroom have different reasons to change. They will share plumbing that branches off so you only need a single water heater and a main shut-off valve, and they will share drains that converge to carry wastewater out. But beyond that, by keeping them in separate rooms, a change in one will never affect the other.
If we take SRP to the extreme, we might find ourselves believing every room can only house a single item (or that every class should only have one method): a room for the faucet, another for the fridge, another for the TV... Michael Feathers already warned us against this hyper-literal interpretation of the principle: it’s about fulfilling a unified purpose.
This is where it becomes vital to differentiate between a law and a heuristic. If you have multiple actions whose overarching purpose is unified (for example, cooking), they belong together; if describing it requires adding coordinating conjunctions (cooking AND showering, or cooking AND taking a dump), then it’s time to put up partition walls.
Robert C. Martin relied on the concept of «actors» to ground single responsibility: a class should answer to a single actor to avoid conflicts of loyalty. This prevents putting your code in the awkward position of answering the classic question: «Who do you love more, mommy or daddy?»—or in its corporate version—: «Who are you going to please today, Accounting or HR?». In software, as in life, no one can serve two masters without eventually betraying one; that’s why we separate classes: so that each has only one boss to report to, and only one reason to change.
Second Heuristic: Open / Closed
Change is a constant in life, and in software too. Continuing with our house analogy, if one day a newly separated relative comes over needing a place to crash for a few weeks, letting him sleep on the couch (meaning, mutating the living room into a bedroom) is a terrible solution because it blows up the normal functionality of the room: say goodbye to watching TV or having a peaceful breakfast while that man is still in his pajamas brooding over his heartbreak. Putting a bunk bed in your kid's room isn't a great idea either; the poor child shouldn't have to pay the price for the snoring that likely caused the divorce in the first place.
The solution lies in extending the house without modifying its structure: an inflatable mattress or a foldaway bed does the trick, easily adding sleeping space without disrupting the daily routine of the rest of the household.
This is the essence of the Open/Closed Principle (OCP): open for extension, but closed for modification. It’s the heuristic that protects us from breaking what already works in production, based on an undeniable mathematical truth: the probability of introducing a bug is directly proportional to the amount of old code you touch.
For a house to accommodate an air mattress without undergoing major renovations, you need a standardized coupling point: a wall socket for the pump and a clear patch of floor. In software, those coupling points are contracts and interfaces. Instead of cramming a switch/case or an endless string of if/else statements into a class that has run peacefully for three years without causing trouble, we leave an abstraction. When the business asks us to support a new payment method, a new shipping gateway, or a new user type, it’s simply a matter of implementing a new class and plugging it in via polymorphism, without touching a single comma of existing code.
Now, here lies the danger of overzealousness: the overengineering trap. Robert C. Martin himself repeatedly warned that it is impossible to 100% future-proof a system against every conceivable change. Trying to anticipate every hypothetical scenario only leads to architectural delusions: you’re not going to build a home with hydraulic trapdoors, retractable roofs, and murphy beds hidden in the walls "just in case" a whole regiment of destitute relatives shows up.
Good design isn't about playing Nostradamus by stuffing the repository with premature abstractions and factories of factories. It’s about identifying the most likely points of friction, designing clean interfaces where modifications would truly hurt, and leaving working code alone until the next real requirement comes knocking at the door.
Third Heuristic: Liskov Substitution
Picture this: after all this mess with home renovations and the brother-in-law who’s spent a month freeloading on your couch and drinking your beer, you’ve developed a neck pain that just won't go away (much like your wife's brother). You call to book an appointment with your lifelong family doctor, only to be told that Dr. Martínez has retired. Annoyed, you search the directory and find another Dr. Martínez: one Juan Martínez. Bingo! It’s the son of your trusted physician.
You schedule an appointment with the offspring, arrive at his office, and find neither an examination table nor a stethoscope: it’s a study lined with books. The son is a doctor, yes, but a Doctor of Philosophy. The only thing he can offer to relieve your cervical spasm is a passage from Marcus Aurelius on enduring suffering through Stoic equanimity.
This misunderstanding perfectly illustrates the principle formulated by Barbara Liskov in 1987: if a class $S$ is a subtype of a class $T$, any program using $T$ should work just as well if passed $S$ without even noticing the swap.
Plainly spoken: inheritance isn't about arm-chair taxonomy; it’s about honoring behavioral contracts. If your code expects an object that knows how to prescribe anti-inflammatories and you pass it a subclass that only knows how to recite Seneca (or worse, throws a MethodNotSupportedException the moment you ask for a prescription), you’ve just blown up the Liskov substitution principle.
In real-world code, LSP violations are those classic duct-tape patches where a developer has to insert an if ($doctor instanceof Philosopher) before calling a method just to keep the system from blowing up. If using a child class forces you to check its specific type first, your inheritance is broken and your design needs immediate surgery.
Fourth Heuristic: Interface Segregation
Sticking with the doctor analogy, it wouldn't make any sense for everyone holding a doctorate in different fields to perform the exact same actions (implementing the exact same interface). Our Doctor of Philosophy has no business implementing a prostateExam() method—seriously, under no circumstance; if he tries, leave the office and call the police immediately.
This perfectly sums up the Interface Segregation Principle (ISP): no client should be forced to depend on methods it does not use.
When we design a "monster" interface (fat interface) bloated with dozens of methods to cover every imaginable variant of a domain, we force implementing classes to carry baggage that doesn't belong to them. In everyday coding, violating this principle leaves an unmistakable trail of second-hand embarrassment: methods implemented with empty bodies and comments reading // TODO: Does not apply here, methods returning a sad null, or classes throwing throw new NotImplementedException() the second someone clicks the wrong button.
To avoid this cross-contamination, the heuristic nudges us toward role interfaces (role interfaces): small, specific, highly cohesive contracts. It is infinitely cleaner and more maintainable to have atomic interfaces like PrescribesMedication, PerformsSurgery, or DiscoursesOnExistentialDread than a single all-terrain DoctorInterface.
If a class needs to fulfill multiple roles, it simply implements multiple small interfaces. That way, you ensure whoever interacts with your code only sees and demands what they actually need to consume, with no nasty surprises on the examination table.
Fifth Heuristic: Dependency Inversion
To bring things full circle, imagine hosting a lunch in your remodeled house with the entire cast gathered around the table: your partner, your kid, your brother-in-law (still camped out on the sofa), and Dr. Martínez (son), whom you've invited to make up for the other day’s medical mix-up.
Lunchtime arrives and the ordeal of catering to everyone’s quirks begins: you know Dr. Martínez is from Valencia, so he’ll inspect the rice dish with inquisitorial scrutiny; your brother-in-law only eats plain white rice with canned tomato sauce; your partner is on a strict diet; and your kid demands frozen pizza.
If your Host class tries to directly satisfy each of these concrete demands, your kitchen collapses. You’ve become a slave to the low-level details of your guests: every time the brother-in-law decides to go vegan or the Valencian gets offended over a pinch of onion, the host has to rewrite all internal logic.
The Dependency Inversion Principle (DIP) breaks this tyranny with a crystal-clear premise: high-level classes should not concern themselves with the quirks of low-level classes.
The high-level class (the host) represents the overarching policy: we serve RiceWithThings at 2:00 PM, period. It’s not the host’s job to manage the dietary neuroses of every single guest; the guests (low-level) are the ones who must implement the common Diner contract and figure out how to adapt:
-
If the Valencian doesn't like seeing bell pepper in the rice, let him discreetly push it to the edge of the plate (internal exception handling).
-
If the kid preferred pizza, let him implement his
fast()method and wait patiently for an afternoon snack. -
If the brother-in-law needs canned tomato sauce, let him get up, go to the pantry, and inject it himself.
By inverting the dependency, the host remains completely decoupled: he only serves food against an interface. Whoever wants a seat at the table must abide by house rules, and if someone cannot or will not adapt to the contract, that’s a defect in their own implementation, not in the host’s architecture.
In conclusion, it’s not about ticking off a checklist with religious fervor or living in fear of a raid by the clean code police. It’s simply a handful of mental tools for making sensible decisions when designing with object-oriented programming and building a system meant to endure over time, receiving continuous maintenance and improvements. You don’t need the SOLID principles for a throwaway script, but you sure do to build software that looks like it was designed by a software engineer rather than a Doctor of Philosophy.
`exit(0);`