SOLID Principles: SRP, OCP, LSP, ISP, DIP in Software Design

SOLID Principles

These principles are used to make software design more understandable, flexible, and maintainable.

  1. Single Responsibility Principle (SRP)
  2. Open Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. Dependency Inversion Principle (DIP)

Single Responsibility Principle (SRP)

Definition: a class should have only one reason to change.

Implementation

The code below violates the SRP principle as it mixes open-gate and close-gate responsibilities with the main function of servicing the vehicle.

public class GarageStation
{
    public void DoOpenGate()
    {
        // Open the gate functionality
    }
    
    public void PerformService(Vehicle vehicle)
    {
        // Check if garage is opened
        // Finish the vehicle service
    }
    
    public void DoCloseGate()
    {
        // Close the gate functionality
    }
}

public interface IGarageUtility
{
    void OpenGate();
    void CloseGate();
}

public class GarageStationUtility : IGarageUtility
{
    public void OpenGate()
    {
        // Open the Garage for service
    }
    
    public void CloseGate()
    {
        // Close the Garage functionality
    }
}

public class GarageStation
{
    private IGarageUtility _garageUtil;
    
    public GarageStation(IGarageUtility garageUtil)
    {
        this._garageUtil = garageUtil;
    }
    
    public void OpenForService()
    {
        _garageUtil.OpenGate();
    }
    
    public void DoService()
    {
        // Check if service station is opened and then
        // Finish the vehicle service
    }
    
    public void CloseGarage()
    {
        _garageUtil.CloseGate();
    }
}

Open Closed Principle (OCP)

Definition: Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

Implementation

The code below violates the OCP principle if the bank introduces a new Account type.

public class Account
{
    public decimal Interest { get; set; }
    public decimal Balance { get; set; }
    
    // Members and function declaration
    public decimal CalcInterest(string accType)
    {
        if (accType == "Regular") // savings
        {
            Interest = (Balance * 4) / 100;
            if (Balance < 1000) Interest -= (Balance * 2) / 100;
            if (Balance < 50000) Interest += (Balance * 4) / 100;
        }
        else if (accType == "Salary") // salary savings
        {
            Interest = (Balance * 5) / 100;
        }
        else if (accType == "Corporate") // Corporate
        {
            Interest = (Balance * 3) / 100;
        }
        return Interest;
    }
}

We can apply OCP by using interface, abstract class, abstract methods, and virtual methods when we want to extend functionality. Here, we have used the interface for example only, but you can go as per your requirement.

interface IAccount
{
    // members and function declaration, properties
    decimal Balance { get; set; }
    decimal CalcInterest();
}

// Regular savings account 
public class RegularSavingAccount : IAccount
{
    public decimal Balance { get; set; } = 0;
    public decimal CalcInterest()
    {
        decimal Interest = (Balance * 4) / 100;
        if (Balance < 1000) Interest -= (Balance * 2) / 100;
        if (Balance < 50000) Interest += (Balance * 4) / 100;

        return Interest;
    }
}

// Salary savings account 
public class SalarySavingAccount : IAccount
{
    public decimal Balance { get; set; } = 0;
    public decimal CalcInterest()
    {
        decimal Interest = (Balance * 5) / 100;
        return Interest;
    }
}

// Corporate Account
public class CorporateAccount : IAccount
{
    public decimal Balance { get; set; } = 0;
    public decimal CalcInterest()
    {
        decimal Interest = (Balance * 3) / 100;
        return Interest;
    }
}

Liskov Substitution Principle (LSP)

Definition: Child class should be substitutable for their parent class. This means if a program is using a base class, then the derived class should be able to extend its base class without changing their original implementation.

Implementation

In the below example, Apple is the base class, and Orange is the child class, i.e., there is a Parent-Child relationship. So, we can store the child class object in the Parent class Reference variable, i.e., Apple apple = new Orange(); and when we call the GetColor, i.e., apple.GetColor(), then we are getting the color Orange, not the color of an Apple. That means the behavior changes once the child object is replaced, i.e., Apple stores the Orange object. This is against the LSP Principle.

using System;
namespace SOLID_PRINCIPLES.LSP
{
    class Program
    {
        static void Main(string[] args)
        {
            Apple apple = new Orange();
            Console.WriteLine(apple.GetColor());
        }
    }
    public class Apple
    {
        public virtual string GetColor()
        {
            return "Red";
        }
    }

    public class Orange : Apple
    {
        public override string GetColor()
        {
            return "Orange";
        }
    }
}

Here, first, we need a generic base Interface, i.e., IFruit, which will be the base class for both Apple and Orange. Now, you can replace the IFruit variable can be replaced with its subtypes, either Apple or Orange, and it will behave correctly. In the code below, we created the super IFruit as an interface with the GetColor method. Then, the Apple and Orange classes were inherited from the Fruit class and implemented the GetColor method.

using System;
namespace SOLID_PRINCIPLES.LSP
{
    class Program
    {
        static void Main(string[] args)
        {
            IFruit fruit = new Orange();
            Console.WriteLine($"Color of Orange: {fruit.GetColor()}");
            fruit = new Apple();
            Console.WriteLine($"Color of Apple: {fruit.GetColor()}");
            Console.ReadKey();
        }
    }
    public interface IFruit
    {
        string GetColor();
    }

    public class Apple : IFruit
    {
        public string GetColor()
        {
            return "Red";
        }
    }
    public class Orange : IFruit
    {
        public string GetColor()
        {
            return "Orange";
        }
    }
}

Interface Segregation Principle (ISP)

Definition: A client should never be forced to implement an interface or methods that doesn’t use.

Implementation

ISP is broken as the process method is not required by the Offline Order class but is forced to implement.

public interface IOrder
{
    void AddToCart();
    void CCProcess();
}
public class OnlineOrder : IOrder
{
    public void AddToCart()
    {
        // Do Add to Cart
    }

    public void CCProcess()
    {
        // process through credit card
    }
}
public class OfflineOrder : IOrder
{
    public void AddToCart()
    {
        // Do Add to Cart
    }
    public void CCProcess()
    {
        // Not required for Cash/ offline Order
        throw new NotImplementedException();
    }
}

We can resolve this violation by dividing the IOrder Interface.

public interface IOrder
{
    void AddToCart();
}
public interface IOnlineOrder
{
    void CCProcess();
}
public class OnlineOrder : IOrder, IOnlineOrder
{
    public void AddToCart()
    {
        // Do Add to Cart
    }

    public void CCProcess()
    {
        // Process through credit card
    }
}
public class OfflineOrder : IOrder
{
    public void AddToCart()
    {
        // Do Add to Cart
    }
}

Dependency Inversion Principle (DIP)

Definition: High-Level Modules/Classes should not depend on Low-Level Modules/Classes. Both should depend upon Abstractions. Secondly, Abstractions should not depend upon Details. Details should depend upon Abstractions.

The most important point you need to remember while developing real-time applications is always to keep the High-level and Low-level modules as loosely coupled as possible.

Implementation

There are different ways to implement Dependency injection. Here, we have used injection through the constructor Injection.

public interface IService 
{
    void Serve();
}

public class Service1 : IService 
{
    public void Serve() 
    {
        Console.WriteLine("Service1 Called"); 
    }
}
public class Client 
{
    private IService _service;
    
    public Client(IService service) 
    {
        this._service = service;
    }
    
    public void ServeMethod() 
    {
        this._service.Serve(); 
    }
}
class Program
{
    static void Main(string[] args)
    {
        // Creating object
        Service1 s1 = new Service1(); 
        
        // Passing dependency
        Client c1 = new Client(s1);
        
        // TO DO:
        c1.ServeMethod();
    }
}