SOLID is an acronym that represents five principles very important when we develop with the OOP paradigm, in addition it is an essential knowledge that every developer must know.
Understanding and applying these principles will allow you to write better quality code and therefore be a better developer.
The SOLID principles were defined in the early 2000s by Robert C. Martin (Uncle Bob). Uncle Bob elaborated some of these and identified others already existing and said that these principles should be used to get a good management of dependencies in our code.
However, in the beginning these principles were not yet known as SOLID until Michael Feathers observed that the initials of these principles fit perfectly under the acronym SOLID and that it was also a very representative name for its definition.
These principles are a set of practical recommendations that when applied to our code helps us to obtain the following benefits:
- Ease to maintain.
- Ease to extend.
- Robust code.
But before we see what each SOLID principle means, we need to remember two relevant concepts in the development of any software.
The coupling and the cohesion:
We can define it as the degree to which a class, method or any other software entity, is directly linked to another. This degree of coupling can also be seen as a degree of dependence.
- example: when we want to use a class that is tightly bound (has a high coupling) to one or more classes, we will end up using or modifying parts of these classes for which we are dependent.
Cohesion is the measure in which two or more parts of a system work together to obtain better results than each part individually.
- example: Han Solo and Chewbacca aboard the Millennium Falcon.
To obtain a good software we must always try to have a low coupling and a high cohesion, and SOLID principles help us with this task. If we follow these guidelines our code will be more robust, maintainable, reusable and extensible and we will avoid the tendency of our code to break in many places every time something is changed.
Let’s break down the letters of SOLID and see the details each of these.
Single Responsibility Principle (SRP):
A class should have only one reason to change.
This principle means that a class must have only one responsibility and do only the task for which it has been designed.
Otherwise, if our class assumes more than one responsibility we will have a high coupling causing our code to be fragile with any changes.
- Coupling reduced.
- Code easier to understand and maintain.
Violation of SRP:
- We have a Customer class that has more than one responsibility:
storeCustomer(String name) has the responsibility of store a Customer into the database so it is a responsibility of persistence and should be out of Customer class.
generateCustomerReport(String name) has the responsibility of generating a report about Customer so also should be out of Customer class
When a class has multiple responsibilities it is more difficult to understand, extend and modify.
We create different classes for each responsibility.
- Customer class:
- CustomerDB class for the persistence responsibility:
- CustomerReportGenerator class for the report generation responsibility:
With this solution, we have some classes but each class with a single responsibility so we get a low coupling and a high cohesion.
Open Closed Principle (OCP):
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification
According to this principle, a software entity must be easily extensible with new features without having to modify its existing code in use.
open for extension: new behaviour can be added to satisfy the new requirements.
closed for modification: to extending the new behaviour are not required modify the existing code.
If we apply this principle we will get extensible systems that will be less prone to errors whenever the requirements are changed. We can use the abstraction and polymorphism to help us apply this principle.
- Code maintainable and reusable.
- Code more robust.
Violation of OCP:
- We have a Rectangle class:
- Also, we have a Square class:
- And we have a ShapePrinter class that draws several types of shapes:
We can see that every time we want to draw a distinct shape we will have to modify the drawShape method of the ShapePrinter to accept a new shape.
As new types of shapes come to draw, the ShapePrinter class will be more confusing and fragile to changes.
Therefore the ShapePrinter class is not closed for modification.
- We added a Shape abstract class:
- Refactor Rectangle class to extends from Shape:
Refactor Square class to extends from Shape:
- Refactor of ShapePrinter:
Now the ShapePrinter class remains intact when we add a new shape type.
The existing code is not modified.
So if we want to add more types of shapes we just have to create a class for that shape.
Now with this solution ShapePrinter class also remains intact when we add a new shape type because the drawShape method receives Shape abstractions.
- We change Shape to an interface:
- Refactor Rectangle class to implements Shape:
- Refactor Square class to implements Shape:
Liskov Substitution Principle (LSP):
Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
This principle was defined by Barbara Liskov and says that objects must be replaceable by instances of their subtypes without altering the correct functioning of our system.
Applying this principle we can validate that our abstractions are correct.
- Code more reusable.
- Class hierarchies easy to understand.
The classic example that usually to explains this principle is the Rectangle example.
Violation of LSP:
- We have a Rectangle class:
- And a Square class:
Since a square is a rectangle (mathematically speaking), we decided that Square be a subclass of Rectangle.
We make overriding of setHeight() and setWidth() to set both dimensions (width and height) to the same value for that instances of Square remain valid.
So now we could pass a Square instance where a Rectangle instance is expected.
But if we do this, we can break the assumptions made about the behaviour of Rectangle:
The next assumption is true for Rectangle:
But the same assumption does not hold for Square:
Square is not a correct substitution for Rectangle since does not comply with the behaviour of a Rectangle.
The Square / Rectangle hierarchy in isolation did not show any problems however, this violates the Liskov Substitution Principle!
- Using a Shape interface to obtain the area:
- Refactoring of Rectangle to implements Shape:
Refactoring of Square to implements Shape:
Another solution that is often applied (with immutability):
- Refactoring of Rectangle:
- Refactoring of Square to extends Rectangle:
Many times we model our classes depending on the properties of the real world object that we want to represent. But it is more important that we pay attention to behaviours to avoid this kind of mistakes.
Interface Segregation Principle (ISP):
many client-specific interfaces are better than one general-purpose interface
This principle defines that a class should never implement an interface that does not go to use. Failure to comply with this principle means that in our implementations we will have dependencies on methods that we do not need but that we are obliged to define.
Therefore, implement specific interfaces is better to implement a general purpose interface. An interface is defined by the client that will use it, so it should not have methods that this client will not implement.
- Decoupled system.
- Code easy to refactor.
Violation of ISP:
- We have a Car interface:
- And a Mustang class that implements the Car:
Now we have a new requirement to incorporate a new car model:
A DeloRean, but it’s not a common DeLorean. Our DeloRean is very special and has the feature to travel in time.
As usual we do not have time to make a good implementation and in addition, the DeloRean has to back to the past urgently.
So we decided:
- Add two new methods for our DeloRean in the Car interface:
- Now our DeloRean class implements the Car:
- But now the Mustang class is forced to implement the new methods to comply with the Car interface:
In this case, Mustang violates the Interface Segregation Principle because should implement methods that do not use.
A solution with interfaces segregation:
- Refactor Car interface:
- Add a TimeMachine interface:
- Refactor Mustang (only implements Car interface):
- Refactor DeloRean (implements Car and TimeMachine):
Dependency Inversion Principle (DIP):
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.
The Dependency Inversion Principle means that a particular class should not depend directly on another class, but on an abstraction (interface) of this class.
When we apply this principle we will reduce dependency on specific implementations and thus make our code more reusable.
- Reduce the coupling.
- Code more reusable.
Violation of DIP:
- We have a DeliveryDriver class that represents a driver that works for a delivery company:
- DeliveryCompany that handles shipments:
Note that DeliveryCompany creates and uses DeliveryDriver concretions. Therefore DeliveryCompany which is a high-level class is dependent on a lower level class and this is a violation of Dependency Inversion Principle.
- We create the DeliveryService interface to have an abstraction:
- Refactor DeliveryDriver class to implements DeliveryService:
- Refactor DeliveryCompany that now depends on an abstraction and not off a concretion:
Now the dependencies are created elsewhere and are injected through the class constructor.
It is important not to confuse this principle with the Dependence Injection that is a pattern that helps us to apply this principle to ensure that collaboration between classes does not involve dependencies between them.
Following SOLID principles is essential if we are to build quality software that is easy to extend, robust and reusable. Also is important not forgetting to be pragmatic and use common sense because sometimes over-engineering can make simple things more complex.
Thanks for reading.
If you liked it, please hit the ❤ icon below.