S.O.L.I.D. is an acronym intended to make software designs more understandable, flexible and maintainable Object-Oriented Programming. These 5 principles are not intended to fix any code errors but they are designed to make it easy for developers to avoid any bad code style, easier code reuse, easier to identify problems, etc. Robert C. Martin promotes them beside a handful of others. These 5 are the important ones in the field of OOP to be recognized.
When you list the acronyms, they might seem a bit complicated. However, when you dive into it, they are pretty simple concepts to understand for OOP worker.
Robert C. Martin preaches this even in his book, Clean Code.
"We want our systems to be composed of many small classes, not a few large ones."
This means we should have single responsibility for each class, not multiple. Let’s take a look at an example:
You have 3 distinct operations here: calculation of paycheck, saving user information to the storage(ex: database), showing the user information. These operations do not require to be coupled to each other. You should create a class for each of these operations so you have a class that does distinct operation such as the one that calculates, one handling the storage and one handling the user information.
The idea is simple.
"Classes should be built to open for extension but closed for modification"
Classes should be designed so you could extend the functionalities easily but the developed class should not be modified except to correct any bugs or issues. The focus sometimes is misinterpreted as “extending” to “inheriting” as the keyword “extend” refers to the “inherit” in many languages. However, this is not true. Principles are older than these modern languages and they do not necessarily mean you should subclass the actual class that needs the new behaviour. We should favour composition over inheritance where favouring the composition produces more malleable and favours polymorphic behaviour rather than using the inheritance where it is more tightly coupled and breaks encapsulation. Let me show an example of what I mean:
Now let’s say we would like to extend this to support multiple different payment methods. We could pass in a boolean flag which controls the payment method like this:
if (credit) payCredit(sum) else payCash(sum). However, this is kind of boolean flag argument (parameter) is a code smell and should be avoided because you are providing 2 different behaviours in one function in contrast to just one. This is probably a better way:
This does what OCP really wants. It provides the support for even more payments methods by leaving the implementation open-ended in a concise and clean manner.
Liskov Substitution Principle (LSP) starts out by saying,
"objects in a work should be replaceable with instances of their subtypes without altering the correctness of that work."
This statement really explains everything. In other words, It’s basically saying not to forcibly fit objects to become one of your models. Here is an example. Say you defined a “Car” in this fashion:
Well, most of the recent modern cars are equipped and comes with a Bluetooth function. What if a car has no Bluetooth functionality at all? like cars made before the year 2000? We can make our ways out and just throw an exception or something or ignore such functionality to make the program flow and define this old car to be our ICar interface. However, this is what’s breaking Liskov Substitution Principle because we end up altering the correctness of the work. A better way to design this would be to rename ICar to such as IModernCar and create another interface that describes older cars. In common sense, old cars definitely categorize into cars, but with the way we have designed the ICar, those old cars are not how the car should be defined and they shouldn’t be categorized as the ICar.
The interface-segregation principle (ISP) states that,
"no client should be forced to depend on methods it does not use."
Within object-oriented design, interfaces provide layers of abstraction that facilitate conceptual explanation of the code and create a barrier preventing coupling to dependencies which means that interfaces should be constructed very specifically to its needs. Let’s describe a Worker who can work and eat at the same time.
We have a problem though. Not all workers require eating. Also, if we have more behaviours added to this “Worker”, the single interface will be cluttered and confusing. Instead, we can simplify this and design in this fashion by segregating interfaces:
You can see that by dividing the works necessary to the specific interfaces, you can manage the different classes according to the necessary action in a much more flexible way. Adapter design pattern usually helps to segregate these fat interfaces.
The principle states:
The way of thinking is “inverted” for OOP designers designing their dependencies. High-level classes are not working directly with low-level classes anymore, they are built through the abstractions. The DIP is about the level of the abstraction in the messages sent from your code to the thing it is calling. It is about the shape of the object upon which the code depends. Let’s start with an example. We have a reader which reads a book in Scala:
Now requirement becomes that you need to be able to read more than just a physical book such as PDF book, physical book, Kindle book, etc. You could build multiple classes to handle each case, but can’t you abstract this? Yes, you can!
You can then use it in a very flexible way. You can apply design patterns like Adapter or Decorator depending on the tasks you are to do. See below for basic usage on above design: