This is a question which I was asked in a senior software engineer position for a Berlin based company.
SOLID is a set of 5 principles, which is followed to make maintainable systems.
S — Single Responsible Principle
O — Open-Closed Principle
L — Liskov Substitution Principle
I — Interface Segregation Principle
D-Dependency Inversion Principle
Single Responsibility Principle(SRP)
A class should have one and only one responsibility. And it should only have one reason to change.
The goal of SRP is to limit the impact of change. If a class has only one responsibility, there will only a few self contained cases to test. When there is a change, the impact will be less(minimal changes in a very few places).
If a class has less functionalities, there will be less coupling with other classes. Only a few client classes will depend on it. So, there will be high cohesion.
Finally, it will be easy to search in codebase, because class name is monolitic.
It’s a sign of trouble if a class does tons of things, if things don’t break now, it will break someday.
In c++ standard template library, std::vector only implements the bare minimum functions, like methods to insert, delete, clear elements, methods to resize, query the size, check emptyness, elements at an index. To be useful with stl algorithims, it provides iterators. By following SRP, std::vector is just stable and self sufficient class.
Classes(or software entities) should be open for extension, but closed for modificationclasses should be open for extension, but closed for modification.
When a class doesn’t change, the client code using those will not change. The test cases doesn’t change. But when you want to add features to a class, you should extend the existing class.
Many modern object oriented concepts enforce it. An interface (or an abstract class in C++) leave a few methods unimplemented. Child class can implement it to add new features.
Similarly a function can take another function as an argument to provide extensibility. Many find variant algorithims in stl take a function argument to enforce open-closed principle.
Here, any container which provides begin() and end() iterator can fully take advantage of the function. UnaryPredicate provides the extensibility. So find_if() is closed for the search algorithim modification, but open for search creteria extension.
Liskov Substitution Principle(LSP)
Subtype must be a substitute for it’s base type.
When a class is inherited from a base class, then the subclass can replace for that base class.If it doesn’t work, it means there is some issue in the inheritance design.
A class should inherit another class if it needs all the functionalies. Otherwise the inherited functionalities will not make sense. If the inheritance doesn’t work, the developer should use delegation model.
Let’s say we have a Car base class and a couple of child classes. All cars can be fueled up.
Now we add Tesla.
Now it caused a lot of problem. You can’t put fuel in Tesla. It simply doesn’t make any sense.
You can remove addFuel() from Cars class and model it as a separate behavior.
Now, we create a member variable in Car, set the behavior during construction.
In this we don’t mess up the inheritance, and our design is still flexible to support more classes.
Interface Segregation Principle(ISP)
Clients should not be forced to depend upon interfaces that they do not use.
Like SRP, ISP tries to minimize the impact of changes. When a client doesn’t need to use an interface, it should not depend on it.
If an interface changes, only the client who uses it, should be modified. In many languages, it also reduces the build time, because dependency is created if it’s necessary.
Dependency Inversion Principle(DIP)
Dependency Inversion refers to the decoupling of software modules. This way, instead of high-level modules depending on low-level modules, both will depend on abstractions.
In a complex software, changes are constant and usually at the implementetation layer. Everytime there is a change in the implementation, if the interface doesn’t change, it’s not necessary to change the clients. That’s why different modules should depend on the interface, not on the implementation.
In languages like C++, DIP saves a lot of build time. And also, if only the implementation changes, only the libraries are updated.