The SOLID design principles: Explained

What are SOLID principles and Why?

Shamoda Jayasekara
Conceptual Viewpoint
6 min readFeb 26, 2021

--

If you are familiar with Object-Oriented Programming, you have probably heard about the SOLID principles. Following these five principles in Software Development, makes it easier to maintain and scale when the project grows. So, don’t dare to violate them. 😉 They were introduced by a popular American Software Engineer called Robert C. Martin (also known as Uncle Bob).

SOLID stands for,

· S —> Single Responsibility Principle

· O —> Open-Closed Principle

· L —> Liskov Substitution Principle

· I —> Interface Segregation Principle

· D —> Dependency Inversion Principle

Now, Let’s take them one by one,

Single Responsibility Principle

It is all about having one and only one reason to change. When we are talking about a class it should have one responsibility.

Let’s take an example.

Well, what do you think about this Person class which has two responsibilities? Is it reusable? Is it easy to maintain? NOOO!!! 😑
But why?? 🙄 Let me show you.

What happens if a class has multiple responsibilities?
Well, it will increase the possibility of bugs and errors because when you make some changes to one responsibility it may affect the other responsibilities as well.

Instead of the above approach, we can create 2 separate classes like this.

The main goal of this principle is to separate responsibilities from each other. It will increase the code reusability and most importantly it will increase the maintainability of the code because changing one behavior will not affect the other unrelated behaviors.

Open-Closed Principle

It says software entities like classes should be open for extension but closed for modification. What does that mean? 🙄 Look at the below code segment.

Consider a scenario where you want to add more Animals to the above program like Lion, Cow, Sheep, etc. You have to keep constantly modify the program and add more if-else blocks to it. It would violate the open-closed principle.

So, how to overcome this problem? I’ll show you one approach, 😉

We can create an abstract class called Animal and declare speak() method inside of it.

Then we can create separate classes for each animal and extend each class to the Animal class. It will enable us to do the implementation of speak() method separately. Look! 👇

See?? Now, if you want to add another animal to the program, just create a class extending to Animal class and do the necessary method implementations inside of it. It’s that simple! 😅
This satisfies the Open-Closed principle. Remember! Open for extension but Closed for modifications. 🤗

Liskov Substitution Principle

Well, sometimes you may extend some classes using inheritance even when it is not the best approach but to reuse some code. Liskov Substitution method says sub-classes must be substitutable for their base-class. Let’s take an example,

Above, we defined a simple Bird class with a couple of methods that all birds can fulfill. Now let’s extend our abstract class to the Eagle class and provide some code lines to its methods.

As our code describes, birds have a Beak and Wings. Let’s take Ostrich as our next example. But wait! 😳 Ostriches can’t really fly??? Exactly, they can’t fly. But it has all the other behaviors of a Bird. So, can we extend the Ostrich class to the Bird class? No!!! because when a child class cannot perform the same actions as its parent class, it can cause bugs and errors. In this case, we have to do separate implementations for our Ostrich class.

We should always be careful when we use inheritance. We should use inheritance only when our base-class is replaceable by a sub-class in all instances. Don’t use inheritance just to reuse few lines of code. 😬

Interface Segregation Principle

It says to keep our Interfaces as small as possible. The moment we have large interfaces with a lot of methods, then every time we change the interface all the implementing classes also should be changed.

Let’s take the previous example again. Initially, the Bird interface has makeSound() method only.

Let’s implement this interface to Eagle and Ostrich classes.

Okay, up to now everything is fine. what if we want to add the fly() method into our Bird interface. If we do that, we have to do its implementation in both Eagle and Ostrich classes. But Ostriches cannot fly. 😐

Therefore, we have to make a dummy implementation of the fly() method in the Ostrich class just to make our compiler happy. 😄 So, how do we overcome this problem? It’s simple let me show you.

Let’s create another interface called Fly and define the fly() method inside of it.

Now, all you need to do is implement Bird and Fly interfaces to the Eagle class. It will not be affected to Ostrich class. Therefore, the Ostrich class will remain the same.

Cool, isn’t it? The aim of this principle is to split a set of behaviors into smaller sets so that a Class executes only the set of behaviors it requires.

Dependency Inversion Principle

First, let’s learn about coupling. Wait! What? 🙄 Coupling??? Yeah, coupling means how much one class knows about another class. Let’s take class A and class B for our example. If class A knows class B through its interface only, for example, class A interacts with class B through its API interface then class A and class B said to be loosely coupled.

On the other hand, assume classes A and B interact with each other using interfaces as well as non-interface stuff, then they are said to be tightly coupled. Suppose you change the class B’s non-interface part, as previous example non-API stuff, then in the case of loose coupling class A does not breakdown, but tight coupling causes class A to breakdown.

Okay, now let’s discuss the Dependency Inversion Principle.

This principle refers to the decoupling of software modules or classes. It says higher-level classes should not be dependent on lower-level classes. Both should depend on abstraction not on concrete classes. In simple terms, always try to depend upon interfaces(abstractions) rather than concrete classes.

You may probably think about what it means by a higher-level class? Well, the classes that implement the business logic are called higher-level classes. 😉

Here is a simple real-world example of database connectivity.

What do you think about this code? In this case, SQLconnection class will be the lower-level class and InsertData class will be the higher-level class. Yeah, it will work. But declaring SQLconnection with a new keyword inside InsertData class, we have tightly coupled these two classes. Now, you can see this code snippet ☝️ violates the Dependency Inversion Principle.

Later, if we wanted to change the database engine, we would have to modify the InsertData class, and this would violate the Open-Closed Principle as well.

So, how to get rid of this tight coupling?

Since both higher-level and lower-level classes should depend on abstraction. Let's create an interface called ConnectionInterface.

Now we can implement this interface to our SQLconnection class then inject the ConnectionInterface into InsertData class.

Excellent! Now our classes are not depending on each other. They are depending only on the interface that we have introduced.

Well, in this article we have taken a deep dive into the SOLID principles. I know SOLID is a little bit confusing topic, but I believe this article gave you a solid understanding of SOLID principles. 😂 Keep Learning, Keep Growing! 💪

Cheers for now!!!

--

--