A guide to understanding the SOLID principles for your next Object-Oriented Design coding project.
More often than not, object-oriented software development involves a team of developers with differing opinions and workflows, resulting in inefficiency when it comes to refactoring or maintaining the codebase.
How then, can we stick to a set of principles that is widely accepted and adopted across the software engineering field?
Introducing the S.O.L.I.D Principles
S.O.L.I.D refers to a set of five principles introduced in 2000 by Robert C. Martin(also known for introducing the Agile Manifesto). The intention is for developers to build object oriented software designs that are understandable, easy to maintain, and easy to extend.
In this article, I will take you through each principle in detail, using real world analogies and examples to help you understand how you can implement this in your next project.
S — Single Responsibility Principle
“A class should have one, and only one, reason to change.”
Imagine you have to represent a postman as an object in your code. You think, well, a postman would need a method to handle sorting different parcels, a method to calculate routes, and also a method to handle invalid mail addresses.
What if you create a class encapsulating all these different responsibilities?
Here we can see that this class violates the Single Responsibility Principle, since the Postman is given too much responsibilities. Instead, we can consider splitting this class up to smaller classes that handle a specific responsibility.
Of course, each class have other methods to serve their responsibility. The thinking behind this is to see if we can separate out classes so that they can be maintained and developed separately.
O— Open/Closed Principle
“Objects or entities should be open for extension, but closed for modification.”
In simple terms, any object or entity that you write should be extendable easily without a need for modification it.
Imagine if you represented a Cat interface in your code. One day, you realised that you need the cat to be able to learn how to chase a dog. You might think that it make sense the Cat interface to be modified to add a new method chaseDog().
Think again. This gives the assumption across your project that all Cats chase dogs. This thinking is obviously flawed and leads to headaches for other developers in your team (and all dogs). Instead, you should allow another interface CrazyCat to extend from Cat interface with the method chaseDog().
Consider approaching these situations with a mindset that anytime you wish to implement a specific method for an object, you should extend from the object instead of modifying it.
L — Liskov Substitution Principle
“Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T”
Ah. That was a mouthful. In layman terms, this means all subclasses should be able to substitute with their parent class.
Take a Bird as a parent class for example. Now, it’s reasonable to say that a Ostrich(being a bird) can be implemented as a subclass of Bird right?
Wait a minute… Ostriches can’t fly! So it can never use the fly method, and thus this violates the Liskov Substitution Principle. Instead, you should do the following:
Now, an Ostrich doesn’t violate the Liskov Substitution Principle(and fly), and anywhere that an Ostrich is used in your code, a FlightlessBird should be able to substitute for it.
Think carefully the next time you design an “is-a” relationship, these can be tricky!
I — Interface Segregation Principle
“A client should never be forced to implement an interface that it doesn’t use or clients shouldn’t be forced to depend on methods they do not use.”
Simply put, having multiple specific interfaces is likely to be better than a really generic interface.
Continuing on our example of our beloved flightless bird, Ostrich, instead of one interface Bird, a better implementation would be to split them it up to multiple interfaces FlightlessBirds, FlightBird, F̶l̶i̶g̶h̶t̶B̶u̶t̶L̶a̶z̶y̶B̶i̶r̶d̶.
Now, each interfaces can have their own appropriate and specific methods, and objects can implement these interfaces accurately.
D — Dependency Inversion Principle
It states that the high level module must not depend on the low level module, but they should depend on abstractions.
An easy to understand real life example would be the relationship between a CEO and different junior employees at a large company. An obvious violation of this principle can be seen in possible design illustration shown below.
In this situation, there is no abstraction between all low level modules to that of the high level module. Now, to get a job done, the CEO has to depend on all of his different employees individually. Instead, adding a layer of abstraction (a manager), decreases the dependency that the CEO needs.
With appropriate abstractions, the CEO (representing a high level module) in your software project can easily maintain its dependencies, allowing for the company (your software project) to scale easily.
It is always helpful to have a set of principles that is acknowledged and understood by the development team. By keeping these principles at the back of your head, you will definitely be a better coder and maybe even make your fellow developers’ lives easier!