SOLID Design Principles

In today’s blog, I am going to discuss SOLID Design Principles. Most of the blogs and videos that I did in the past uses one or other SOLID Design Principles. But I never talked in detail about all the design principles which are part of the SOLID Design Principles.

Hence, it would be a nice idea to walk through all of them and just discuss about my philosophy around using these design principles.

So to do that, first let us understand what is SOLID design principles?

Introduction of SOLID

SOLID design principles are arguably the most popular design principles for object-oriented software development. And in most cases in modern architecture and design, SOLID principles are used.

SOLID principles as I already mentioned it is a set of design principles. There is a total of five design principles that constitute the SOLID. And the acronym came from the fast letter for each of the principles.

  • The first one is the Single responsibility principle
  • Secondly comes the Open/Closed principle
  • After that comes the Liskov substitution principle
  • Then the fourth one is the Interface Segregation Principle
  • Finally, the fifth one is the Dependency Inversion Principle

In my experience out of the five design principles four of them, we probably would be using every single day. And they are the Single Responsibility Principle, the Open/Closed Principle, the Interface segregation principle, and the Dependency inversion principle.

That does not mean that the Liskov Substitution Principle is not important. It is also extremely important. But it is not applicable to all situations.

Single Responsibility Principle (The S of SOLID)

Firstly let us start with the Single Responsibility Principle.

So what is the Single Responsibility Principle? In the Single Responsibility Principle, the concept is that a class should have only one responsibility hence only a single purpose to live.

So you should never overburden a class with multiple responsibilities. That is the whole purpose of the single responsibility principle.

In my videos and blog posts, Where I spoke about micro-services, sometimes I synonym micro-services with single responsibility services. It is the same principle at a service level when a service contains only one responsibility.

If you boil down that principle into individual classes, then you are basically saying that classes should have a single responsibility. And that is the single responsibility principle.

Benefits of the Single Responsibility Principle

There are few benefits of using the single responsibility principle. There might be more than what I listed. But in my opinion, these few are very important.

  • Firstly, given that when you implement a single responsibility principle the classes are very concise, it is extremely easier to understand.
  • Secondly, it is extremely easier to maintain as well. Because the classes do not deal with multiple things.
  • Thirdly, since the classes do not deal with multiple things, they deal with only single responsibility the chances of it getting changed are also very less. It is because when a requirement changes, not every part of the application is going to change. Hence if all the classes are single responsible, the potential that every class getting change is extremely low.
  • Fourthly, given the classes are small, it is easily testable. And also you can test it thoroughly because you have to deal with a much lesser amount of code.

Example of the Single Responsibility Principle

Now let us take a look into a real-life example of what I mean by the single responsibility principle.

So let us say we have this order processor class and the responsibility of the class is to process an order. So what are the different parts of order processing?

  • Firstly, validation of the order
  • Secondly, saving the order
  • Finally notifying the customers that the order has been successfully executed

The class will look something like below:

using System;

namespace SOLID.Demo
{
    public class OrderProcessor
    {
        public void Validate() { }
        public void Save(string order) { }
        public void SendNotification() { }
    }
}

So if you look into the above implementation, you can see that the OrderProcessor right now is doing too many things. It is validating the order, saving the order, and finally, it is sending a notification confirmation.

Now if we break this class into three single responsibility classes, it will look something like below:

using System;

namespace SOLID.Demo
{
    public class OrderProcessor
    {
        private readonly OrderValidator orderValidator;
        private readonly OrderSaver orderSaver;
        private readonly OrderNotificationSender orderNotificationSender;

        public OrderProcessor(OrderValidator orderValidator, OrderSaver orderSaver, OrderNotificationSender orderNotificationSender)
        {
            this.orderValidator = orderValidator;
            this.orderSaver = orderSaver;
            this.orderNotificationSender = orderNotificationSender;
        }

        public void Process()
        {
            orderValidator.Validate();
            orderSaver.Save(null);
            orderNotificationSender.SendNotification();
        }
    }

    public class OrderValidator {
        public void Validate() { }
    }

    public class OrderSaver
    {
        public void Save(string order) { }
    }
 
    public class OrderNotificationSender
    {
        public void SendNotification() { }
    }
}

So now what we have done is, we have broken down the OrderProcessor class into three single-responsibility classes. And each class is responsible for a very specific thing.

And the OrderProcessor class now just acts as an orchestrator for the single responsibility classes.

Now it becomes if you look back into the benefits, you can see that all the benefits are very evident from the example.

Open/Closed Principle (The O of SOLID)

The Open/Closed principle says that a class should be open for extension but closed for modification. So what are the benefits of the Open/Closed principle?

  • Firstly, not allowing modification, provides the advantage of not introducing bugs. Because if you are not allowing modification to the existing code, the chances of introducing bugs are going to be significantly low.
  • Secondly, all dependent classes will not have to adapt to the modification

So that is in my opinion are the fundamental benefits of the Open/Closed principle.

And in essence, if you think about it, if you are using an interface, you are using the Open/Closed principle.

Example of the Open/Closed Principle

Let us consider that the OrderSaver saves orders into an RDBMS database. If a requirement comes that apart from saving in the RDBMS database, the order needs to save in a distributed cache as well for a faster read.

Now as a natural instinct, we might introduce a new method inside the OrderSaver, for saving data in the cache.

public class OrderSaver
{
     public void Save(string order) { }
     public void SaveCache(string order) { }
}

In the future, if we have to save in another place, let us say distributed database like or Cassandra. Now we will need to introduce another new method.

As you can see that this class started getting really messy and it will start becoming really big. And also it is going to break into the single responsibility principle.

So we can solve this through the Open/Closed design principle. Which as I mentioned earlier is to keep it closed for modification but open it up for an extension.

Hence, we will create an interface IOrder. By declaring this interface, we will open it up for future extensions. Let me update the code to show how it will look like.

using System;

namespace SOLID.Demo
{
    public class OrderProcessor
    {
        private readonly OrderValidator orderValidator;
        private readonly IOrderSaver[] orderSaver;
        private readonly OrderNotificationSender orderNotificationSender;

        public OrderProcessor(OrderValidator orderValidator, IOrderSaver[] orderSaver, OrderNotificationSender orderNotificationSender)
        {
            this.orderValidator = orderValidator;
            this.orderSaver = orderSaver;
            this.orderNotificationSender = orderNotificationSender;
        }
        public void Process()
        {
            orderValidator.Validate();
            foreach (var item in orderSaver)
            {
                item.Save(null);
            } 
            orderNotificationSender.SendNotification();
        }
    }

    public class OrderValidator {
        public void Validate() { }
    }

    public interface IOrderSaver
    {
        void Save(string order);
    }
     
    public class DbOrderSaver : IOrderSaver
    {
        public void Save(string order) { }
    }
 
    public class CacheOrderSaver : IOrderSaver
    {
        public void Save(string order) { }
    }

    public class OrderNotificationSender
    {
        public void SendNotification() { }
    }

}

As you can see from above, now we have the ability to extend the interface with multiple implementations and not modify the existing type.

Liskov Substitution Principle (The L)

Liskov Substitution states that a subclass should be substitutable by its base class without having any negative impact on the caller.

The benefits of the Liskov Substitution Principle are:

  • Callers do not get surprising behavior when substitution applies
  • The complex bug which might arise due to conflicting behavior between inheritance is avoided easily.

Example of Liskov Substitution Principle

For an example of the Liskov Substitution Principle, let us consider we have a class called Bird. And let us consider it has a method Fly.

What is going to happen is, if we declare another class and we call it Ostrich which extends the Bird. Now a class expects type Bird as a parameter and calls the Fly. Now if we substitute Bird with an instance of Ostrich, the Fly method in Ostrich does not make any sense.

This is an example where the Liskov Substitution Principle is broken.

namespace SOLID.Demo
{
    public class Bird
    {
        public void Fly() { }
    }

    public class Ostrich : Bird { }
}

So to implement the Liskov Substitution Principle, we will declare Bird as a base class without the Fly method. And then we can have another class called FlyingBird which inherits from Bird. And the FlyingBird will implement the Fly method.

Now the Ostrich class can derive from Bird. Whereas another class Pigeon can derive from the FlyingBird.

So now if we replace Bird with an instance of Ostrich, we are not going to break Liskov Substitution Principle. And then if you are passing a FlyingBird, that can be exchanged with Pigeon.

namespace SOLID.Demo
{
    public class Bird
    {
    }

    public class FlyingBird : Bird{
        public void Fly() { }
    }

    public class Ostrich : Bird { }

    public class Pigeon : FlyingBird { }
}

And as I was mentioning earlier, this is not something you will have to deal with every day. We do not come across this kind of scenario in day-to-day problems that we solve.

Interface Segregation Principle

The Interface Segregation Principle is all about separating interfaces. Basically, multiple specific interfaces are better than one generic gigantic single interface, that is what interface segregation is all about.

The benefits of interface segregation are, of course, all the benefits of the single responsibility principle.

Because if you segregate the interface you will end up having more chances of having single responsibility. And also the classes which implement the interfaces are going to become much smaller.

There are certain times if you have an interface with five methods and one class really needs just four you end up having an interface that doesn’t have an implementation.

And this creates all kinds of bad behavior. So you either don’t return anything from a method or throw an exception. It is just ugly.

So interface segregation in my opinion is very critical. And as I mentioned it works hand in hand with the single responsibility principle.

Example of Interface Segregation

As an example, if we have a requirement to delete and read order apart from the save. Now we can extend the IOrderSaver to add two new methods. This will cause the interface to be overloaded, and also it will not be a single responsibility.

public interface IOrderSaver
{
    void Save(string order);
    void Delete(int id);
    string Read(int id);
}

To fix this, we can create two more interfaces to handle the delete and read functionality. This will create interface segregation.

public interface IOrderSaver
{
    void Save(string order);
}

public interface IOrderDeleter
{
    void Delete(int id);
}
    
public interface IOrderReader { 
    string Read(int id);
}

Dependency Inversion Principle

And the last, but not the least is the Dependency Inversion Principle.

According to the Dependency Inversion Principle, classes should only depend on contracts, meaning interfaces or abstract classes rather than concrete implementations

As you can see this is very closely related to Open/Close Principle.

In our example of OrderProcessor, we do not have any interface associated with the OrderNotificationSender and OrderValidator. To comply with the Dependency Inversion Principle, these two classes need to implement an interface.

The benefit is first, you can decide what implementation you pass. Secondly, when it comes to unit testing, it becomes extremely easy to test because you depend on interfaces rather than concrete classes. You can always have a mock implementation during unit testing.

using System;

namespace SOLID.Demo
{
    public class OrderProcessor
    {
        private readonly IOrderValidator orderValidator;
        private readonly IOrderSaver[] orderSaver;
        private readonly IOrderNotificationSender orderNotificationSender;

        public OrderProcessor(IOrderValidator orderValidator, IOrderSaver[] orderSaver, IOrderNotificationSender orderNotificationSender)
        {
            this.orderValidator = orderValidator;
            this.orderSaver = orderSaver;
            this.orderNotificationSender = orderNotificationSender;
        }
        
        public void Process()
        {
            orderValidator.Validate();
            foreach (var item in orderSaver)
            {
                item.Save(null);
            } 
            orderNotificationSender.SendNotification();
        }
    }

    public interface IOrderValidator
    {
        void Validate();
    }

    public class OrderValidator : IOrderValidator
    {
        public void Validate() { }
    }

    public interface IOrderSaver
    {
        void Save(string order);
    }

    public interface IOrderDeleter
    {
        void Delete(int id);
    }
    
    public interface IOrderReader { 
        string Read(int id);
    }
    
    public class DbOrderSaver : IOrderSaver
    {
        public void Save(string order) { }
    }
    
    public class CacheOrderSaver : IOrderSaver
    {
        public void Save(string order)
        {
            throw new NotImplementedException();
        }
    }

    public interface IOrderNotificationSender
    {
        void SendNotification();
    }

    public class OrderNotificationSender : IOrderNotificationSender
    {
        public void SendNotification() { }
    }

}

As you can see now both the dependency for both these classes can be inverted.

Conclusion

This was an introduction to the SOLID Design Principles. And I hope this clarifies some of the basic concepts and implementation for SOLID design principles.

I have captured the complete session in my YouTube channel here.