First conceptualized by Robert C. Martin in 2000 and later built upon by Michael Feathers.
In last 20 years, these 5 principles have revolutionized the world of object-oriented programming, changing the way we write software.
It encourages us to create more maintainable, understandable, and flexible software.
Consequently, as our applications grow in size, we can reduce their complexity and save ourselves a lot of headaches later.
5 Concepts that make up the SOLID principles.
Single Responsibility
Open - Closed
Liskov Substitution
Interface Segregation
Dependency Inversion
1. Single Responsibility Principle (SRP)
A class should have only one responsibility and It should only have one reason to change.
When designing our classes, we should aim to put related features together, so whenever they tend to change they change for the same reason. And we should try to separate features if they will change for different reasons. —- Steve Fenton
How does this principle help us to build better software ?
Testing – A class with one responsibility will have far fewer test cases.
Lower coupling – Less functionality in a single class will have fewer dependencies.
Organization – Smaller, well-organized classes are easier to search than monolithic ones.
Note :- This principle applies not only to classes, but also to software components and microservices.
If the answer includes the word “and”, we are most likely breaking the single responsibility principle.
Then it’s better to take a step back and rethink the current approach.
There is most likely a better way to implement it.
With the proper application of these, our application becomes highly cohesive.
2. Open-Closed Principle (OCP)
Software entities(Classes, modules, functions) should be open for extension but not for modification.
This principle is the foundation for building code that is maintainable and reusable. —- Robert C. Martin
How can something be open and closed ?
A class follows the OCP if it fulfills these two criteria:
Open for extension : This ensures that the class behavior can be extended. As requirements change, we should be able to make a class behave in new and different ways, to meet the needs of the new requirements.
Closed for modification : The source code of such a class is set in stone, no one is allowed to make changes to the code.
Example:
Let’s imagine we have a store, and we give a discount of 20% to our favorite customers using this class:
Through abstractions, to be able to extend the behavior of a class without changing a single line of code, we need to make abstractions.
For example, if we had a system that works with different shapes as classes, we would probably have classes like Circle, Rectangle, etc.
In order for a class that depends on one of these classes to implement OCP, we need to introduce a Shape interface/class.
Then, wherever we had Dependency Injection, we would inject a Shape instance instead of an instance of a lower-level class.
This would give us the luxury of adding new shapes without having to change the dependent classes’ source code.
How do we know whether to make Shape a class or an interface ?
For that, we’ve got the Liskov Substitution Principle, which tells us when inheritance is suitable.
3. Liskov Substitution Principle (LSP)
A sub-class must be substitutable for its super-class.
What is wanted here is something like the following substitution property:
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T. —- Barbara Liskov
What is LSP ?
Extends the Open/Closed principle and enables us to replace objects of a parent class with objects of a subclass without breaking app.
The aim of this principle is to ascertain that a sub-class can assume the place of its super-class without errors.
If the code finds itself checking the type of class then, it must have violated this principle.
This violates the LSP principle, (and also the OCP principle).
It must know of every Animal type and call the associated leg-counting function.
With every new creation of an animal, the function must modify to accept the new animal.
To make this function follow the LSP principle, follow this LSP requirements postulated by Steve Fenton:
If the super-class (Animal) has a method that accepts a super-class type (Animal) parameter. Its sub-class(Pigeon) should accept as argument a super-class type (Animal type) or sub-class type(Pigeon type).
If the super-class returns a super-class type (Animal). Its sub-class should return a super-class type (Animal type) or sub-class type(Pigeon).
We see that it is impossible to implement a shape that can draw a circle but not a rectangle or a square or a triangle.
We can just implement the methods to throw an error that shows the operation cannot be performed.
ISP frowns against the design of this IShape interface.
Clients (here Rectangle, Circle, and Square) should not be forced to depend on methods that they do not need or use.
Also, ISP states that interfaces should perform only one job (just like the SRP principle) any extra grouping of behavior should be abstracted away to another interface.
To make our IShape interface conform to the ISP principle, we segregate the actions to different interfaces:
So now, no matter the type of Http connection service passed to Http it can easily connect to a network without bothering to know the type of network connection.
We can now re-implement our XMLHttpService class to implement the Connection interface.
We can also create many Http Connection types and pass it to our Http class without any fuss about errors.
Now, we can see that both high-level modules and low-level modules depend on abstractions.
Http class(high level module) depends on the Connection interface(abstraction) and the Http service types(low level modules) in turn, also depends on the Connection interface(abstraction).