SOLID principles are powerful tools for building a system with low coupling between its components. A quick recap on these principles:
- SRP: Single Responsibility Principle
- OCP: Open Closed principle
- Liskov Substitution Principle
- Interface Segregation
- Dependency Inversion
If you don’t know what these terms mean, I recommend this primer. Go check it out and then read the rest of this article.
Here, I want to talk about how all the SOLID principles are interlinked. They all apply simultaneously in any situation. Breaking one will also break multiple others. In my opinion, they should be read as a continuum, rather than a set of independent principles – one always needs some of them to achieve the other(s).
Personally, I start by saying that I don’t want to modify the existing code. Who knows how I might break it? I want to just inject my new logic into the currently running system in the specific places where I need to. So SRP is kind of my favourite principle. But all other principles come in to uphold this one.
Let’s consider the windows machine example from the primer I linked above. Here’s the class for reference.
The machine has a Keyboard, a CPU and a Monitor. In the example, the machine has the responsibility of creating these objects. It needs to not only know how it does its own functions but also how to create these subcomponents. This breaks SRP.
How can we remove the knowledge of building the monitor from the machine? The easiest way is to let someone else build the monitor object and give it to the machine class during construction (much as it happens in the real world). This is the dependency inversion principle – we are using one SOLID principle to achieve another.
But if anyone can construct a monitor and pass it to a machine, the machine needs to be sure that the monitor is compatible with the implementation of the machine. Otherwise, the machine has to handle the differences between various types of monitors. This again breaches SRP. The machine wants others to create monitors, but all monitors must do exactly what the machine expects, regardless of how they do it. How do we get this?
Enter Liskov Substitution Principle, which requires subclasses to do exactly this. The machine class exposes some interface or base class that comprises the “specs” of the monitor. Every monitor must do exactly that, and nothing else. This ensures that the machine can be given any implementation of the base monitor class to work with and nothing in the machine code needs to change. Yet again, we see how one principle supports another.
To the extent that the behaviour of the machine is controlled by the behaviour of the monitor, we have already achieved OCP because we can pass in different monitor implementations to do the same thing in different ways. But how can we modify the behaviour of the machine itself? One way would be to open up the machine’s code and add some conditional or additional business logic there to cater to our new requirements. But if we do this, the final artefact still breaches SRP in a way since it changes for multiple behavioural reasons now.
To prevent this, we must redesign the machine component in a way that allows others to subclass it and override it with new behaviour. The old code is still in play for existing use cases, but wrappers can now be built around it to support new use cases. Here, OCP is helping maintain SRP in the long run.
Let’s look at the monitors themselves. In isolation, they can have many many attributes. Monitor manufacturers deal with tons of complexity. But all of that is not relevant to the machine. So the machine-facing part of the monitor implements a much narrower set of specifications that the manufacturing-facing part. This is the interface segregation principle, where the monitor object implements two different sets of interfaces for two different use-cases.
As this example shows, SOLID principles cannot be applied one by one. They have to be applied all at the same time to achieve the decoupling we want in our systems.
An evolutionary take
An interesting way to look at this is in terms of system evolution. Everywhere in the world, evolving systems develop greater degrees of specialization for every type of component, and they develop a rich collection of different types of components. The cross-play of both these axes results in the immense diversity of living systems we see around us.
I have written before about the mechanics of software evolution. That article painted a higher-level picture of system evolution. Let’s consider how we can guide this evolution inside our components.
Software programs are living, growing systems. SOLID principles are the guiding forces that let the system evolve specialization and diversity in a healthy way, instead of collapsing into a mess of chaos.
SRP and OCP create specialization. SRP is the restriction that prevents a component from becoming too muddled internally. But due to external business pressures, the same component MUST do different things. The pressure builds, and OCP relieves it by allowing a deeper subtree of more and more specialized subclasses which can satisfy business needs.
We saw in the above machine example how the concept of monitor evolved from inside the concept of the machine due to SRP. Similar things can happen with CPU and mouse and so on. Here too, SRP is the forcing constraint, and the Liskov Substitution Principle, Interface Segregation, and Dependency injection jump in to satisfy the constraint by creating diverse types of components, most of which can work with each other.
Once outside, monitor and the other concepts take on a life of their own, each developing its own hierarchy of specialized subclasses. And hence the cycle repeats, creating an increasingly large but consistently decoupled system.
If you liked this, subscribe to my weekly newsletter It Depends to read about software engineering and technical leadership