SOLID Principles — 1/2 — explained

SOLID are the 5 principles of software design. They were introduced by Robert C. Martin (Uncle Bob), in his 2000 paper Design Principles and Design Patterns. For developers, they are a foundation, on top of the 4 Object Oriented Programming (OOP) principles. As a remind, they are Polymorphism, Encapsulation, Inheritance and Abstraction.
SOLID stands for :
- Single Responsibility Principle (SRP)
- Open-Closed Principle Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
These 5 principles are meant to be combined together. When applied to code design, so they will make software easier to maintain, extend and re-use, and the code easier to understand. Developers would produce cleaner code.
Reader’s guide
This article may be a bit too long to read, but in Software development, SOLID principles are to be used and applied together as a whole, it makes more sense to have them explained within the same post. To help the reader to better understand them, samples of code will be shown with applications of each of the 5 principles. These samples are part of a codebase as a whole, let’s call it legacy code. From SRP to DIP, we will see that legacy code change continuously, step by step. You may of course skip one or many principles if you do not find them useful, or to gain an overview you need only read the first paragraph of each section.
Single Responsibility Principle — SRP
This principle will help to cut things in smaller pieces to have more focused meaning or purpose of that one part of code. Typically, instead of having one big class that does everything — UserSearchService doing search and display — we want to have smaller classes and smaller methods so each one has its own responsibility and task.
Statement :
A class should have one and only one reason to change. It should do only one thing.
This principle states that one module or class should have one responsibility. It can be summarized with this statement : “Do one thing and do it well”.
Open Close Principle — OCP
This principle will allow us to make sure our code can embrace business changes, while limiting modification to the code. Typically we want to modify code in one place to add or change a functionality, e.g. order by firstname or age.
Statement:
An Object or an entity should be open for extension, but closed for modification.
It states that software entities (classes, modules, functions, …) should be open for extension, but closed for modification. Using polymorphism can help to comply with this, as we can substitute different implementations (behaviours) as subtypes of class/interface referenced in a caller class. Then we can add new behaviour to the existing design of code. Typically, some design patterns, such as strategy, are made compliant to this principle.
Liskov Substitution Principle — LSP
This principle allows us to use abstraction of classes to add more logic, without breaking any existing functionality. For example, we may want to use a level of abstraction for the display functionality, having one class per type of order, so that if we want to be able to display the result in some other order, we just need to add a class that extends the abstract class.
It states as followed:
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.
It states that an object in a program should be replaceable with instances of its subtype without altering the correctness of that program. This is an extension of both Polymorphism and Inheritance principles. Every subclass should be substitutable for their parent class. Inheritance will enable classes to polymorphically substitute for each other. Still the behaviour is not changed. The objects of your subclasses must behave in the same way as the objects of your superclass. In other words, you should be able to use any derived class instead of a parent class and have it behave in the same manner without modification.
To achieve that, we must follow these two rules :
- An overridden method of a subclass needs to have the same argument values as the method of the superclass.
- The overridden method must return the subclass of the value or a subset of the valid return values from the superclass.
Here, the behaviour of your classes is more important than its structure. The compiler normally checks the structure, but it can’t enforce a specific behaviour.
Interface segregation Principle — ISP
This principle addresses a design problem when we use interfaces. Nowadays software is mostly designed with multiple layers and is service oriented, so we are required to expose some API. When exposing APIs, we need to use interfaces. One of the mistakes we often see are big interfaces, then when we need to add an implementation, we have to implement methods that are not required at all. For example, we would throw an exception, such as UnsupportedOperationException, to make sure the client sees it. And that is exactly what we want to avoid. We can split the ResultDisplayer into smaller interfaces so that when we need a ResultOrderByFirstname API, we just need to implement that method only.
Statement:
A client should never be forced to implement an interface that it doesn't use or a client shouldn't be forced to depend on methods it does not use.
It states that no client should be forced to depend on methods it does not use. Don’t create big interface with many methods that a client won’t need, because it will have to implement those methods.
Dependency Inversion Principle — DIP
This last one is the most seen but is often mis-interpreted as the Dependency Injection design pattern. On one hand, it’s very similar and on the other hand, it’s more about the level of detail. We must make sure that the service level (high-level) does not have any dependency on the ordering level (low-level).
Statement:
A high-level module should not depend on low-level module. Both should depend on abstractions. Abstractions should not depend on details, but details should depend on abstractions.
When a class knows explicitly too much about the details of another class, changes to one class may break the other class. That’s what we call tightly coupled. So we must keep these high-level and low-level modules/classes loosely coupled. To do that, we need to make both of them dependent on abstractions. Then we use a well known design pattern called dependency injection.
Conclusion
Now we have a cleaner code, by using one or many of the SOLID principles at each step.
The first principle — SRP — is quite easy to comply with. The second — OCP — is a bit harder, but using polymorphism and inheritance helps. The third principles — LSP — is a lot harder to understand and to satisfy. The fourth principle — ISP — is not so complicated and we must use it when having abstractions so using it along with SRP could be beneficial. The last — DIP — is a familiar and regularly used. Without DIP, all the other principles would not be that powerful. It is important to take away that these principles are to be used together most of the time.
Github Repo: Kata SOLID principles
Acknowledgement
I would like to thank Dan MAGIER and Geoffroy VERGNE for reviewing and providing me insights, and James FLYNN to correct my English mistakes.