SOLID Design Principles

SOLID Design Principles

Introduction:-

SOLID is an acronym for the basic five object oriented design (OOD) principles promoted by Robert C. Martin, Reason to follow these principles is to manage/maintain software app design, make them easy to understand, flexible. It is first introduced by Michael Feathers.

Single Responsibility Principle (SRP)

A class should have one and only one reason to change, meaning that a class should have only one job.

Let’s take a real world example.

Consider an application that manages employee data. 

//Violation of Single Responsibility Principle (SRP)
class Employee
{
    public int? EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public string CNIC { get; set; }

    public List<Employee> FetchEmployeeRecords()
    {
        //Performs some action to fetch employee records.
        return new List<Employee>();
    }

    public void SaveEmployee(Employee model)
    {
        //Performs some action to save employee
    }

    public void UpdateEmployee(Employee model)
    {
        //Performs some action to update employee
    } 

    public double? CalculateMedicalAllownce(double salary)
    {
        //Performs some actions to calculate medical allownces
        return 0;
    }

    public double? CalculateProvidentFund(double salary)
    {
        //Performs some actions to calculate provident fund
        return 0;
    }

}        

In the above mention code snippet I have added a class named “Employee” with some properties. Take your time and look what violates Single Responsibility Principle in above example.

Yes, It is the class performing multiple action like calculates medical allowances, calculates provident funds and save/update employee information.

Means this class has multiple responsibilities of performing different tasks which violates single responsibility principle.

Now take a look at this example.

class Employee
{
    public int? EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public string CNIC { get; set; }
} 

class ManipulateEmployeeData
{
    public void SaveEmployee(Employee model)
    {
        //Performs some action to save employee
    }

    public void UpdateEmployee(Employee model)
    {
        //Performs some action to update employee
    }
}

class CalculateEmmployeeAllownces
{
    public double? CalculateMedicalAllownce(double salary)
    {
        //Performs some actions to calculate medical allownces
        return 0;
    }

    public double? CalculateProvidentFund(double salary)
    {
        //Performs some actions to calculate provident fund
        return 0;
    }
}        

Performing same task but the approach is different we have now two different classes ManipulateEmployeeData and CalculateProvidentFund these two classes are not in common tasks easy to understand more flexible. In such manner we can add more methods without any hurdle.

Open Closed Principle

The Open-Closed Principle requires that classes should be open for extension and closed to modification.

Modification means changing the code of an existing class, and extension means adding new functionality.

So what this principle wants to say is: We should be able to add new functionality without touching the existing code for the class. This is because whenever we modify the existing code, we are taking the risk of creating potential bugs. So we should avoid touching the tested and reliable (mostly) production code if possible.

Now take a look at this example

class Employee
{
    public int? EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public string CNIC { get; set; }

    public double? Calculatealary()
    {
        //some logic to calculate salary
        return 0;
    }
}        

This example shows the method to calculate the salary of an employee does the job very well. But what if new requirement come up to calculate the salary with allowances and without allowances.

If we do any modification in calculate salary method this will be violation of Open-Close Principle. To fulfill this requirement we will create a new interface name “IManipulateEmployeeData” add method CalculateSalary and implement this method to two different classes calculates the salary with allowances and without allowances.

Here is the code example:

interface IManipulateEmployeeData
{
    public double? CalculateSalary(int EmployeeId);
}

class SalaryCalculatorWithAllownces : IManipulateEmployeeData
{
    public double? CalculateSalary(int EmployeeId)
    {
        //Some logic to calculate salary with allownces
        throw new NotImplementedException();
    }
}

class SalaryCalculatorWithoutAllownces : IManipulateEmployeeData
{
    public double? CalculateSalary(int EmployeeId)
    {
        //Some logic to calculate salary without allownces
        throw new NotImplementedException();
    }
}        

Liskov Substitution Principle

The Liskov Substitution Principle states that subclasses should be substitutable for their base classes.

 

This means that, given that class B is a subclass of class A, we should be able to pass an object of class B to any method that expects an object of class A and the method should not give any weird output in that case.

 

In simple this principle is just an extension for the Open Close Principle and it means that we must make sure that the new derived classes are not changing the behavior of parent class.

Here is the explaination of this principle with the real-world example.

Let’s suppose a father is a civil engineer and his son wants to be a pilot here son can not replace the father even though they belong to same family hierarchy.

 

Code Example:

Suppose we need to build an application that save and read text files

We will create a simple FileHelper  class with properties of “FilePath”, “FileText” a method “ReadText”  which load the text of the file and finally save file method “SaveText” .

public class FileHelper
{
    public string FilePath { get; set; }
    public string FileText { get; set; }
    public string ReadText()

    {
        //code here to read file
        return "";
    }

    public string SaveText()
    {
        //code here to save text file
        return "";
    }
}        

Afterwards we will create the class FileManager for the implementation of FileHelper class.

public class FileManager
{
    public string GetTextFromFiles()
    {
        var obj = new FileHelper();
        obj.FilePath = "";
        obj.FileText = "";
        return  obj.ReadText();
    }

    public string SaveTextFile()
    {
        var obj = new FileHelper();
        obj.FilePath = "";
        obj.FileText = "";
        return obj.SaveText();
    }
}        

We are done. After some time client may tell us we may have some readonly files and we need to restrict whenever user tries to modify on them.

For this purpose we create a new class ReadOnlyFile and inherited it with the FileHelper class

public class ReadOnlyFile : FileHelper
{
    public string FilePath { get; set; }
    public string FileText { get; set; }

    public string LoadText()
    {
        return "";
    }

    public void SaveText()
    {
        //Avoid saving files
        throw new IOException("Don't Save");
    }
}        

But the difference is in SaveText method we are throwing exception in it.

And we added a condition in SaveTextFile incase of readonly files

if(! objFile is ReadOnlySqlFile)

         objFile.SaveText();           

The point is we altered the SaveTextFile method to determine if the case is readonly file this is the substitution for its parent and this above example violates LSP Principle.

To resolve this we will create two interfaces 

public interface IReadableFile
{ 
   string LoadText(); 
} 

public interface IWritableFile 
{ 
   void SaveText(); 
}           

Inherit IReadableFile  with ReadOnlyFile 

public class ReadOnlyFile: IReadableFile
{ 
   public string FilePath {get;set;} 
   public string FileText {get;set;} 

   public string LoadText() 
   { 
      /* Code to read text from sql file */ 
   } 
}          

And also implement the both IReadableFile and IWritableFile  with FileManager Class

public class FileManager: IWritableSqlFile,IReadableSqlFile
{ 
   public string FilePath {get;set;} 
   public string FileText {get;set;} 

   public string LoadText() 
   { 
      /* Code to read text from sql file */ 
   } 

   public void SaveText() 
   { 
      /* Code to save text into sql file */ 
   } 
}           

Now we can say our design is following the LSP. And we fixed the problem using the Interface segregation principle by (ISP) identifying the abstraction and the responsibility separation method.

Interface Segregation Principle

Interface Segregation Principle (ISP) is used to resolve the design problem of the application it can be explained as when all the tasks are done by a single class then it has become the fat class with overburden of tasks, and inheriting this class will be resulting sharing methods which are not required in the derived classes.

Here is the example implementation

public interface IOrderPlacement
{
    void PlaceAnOrder();
    void ProceedToPayment();
    void CalculateShipment();
}        

Firstly Create an interface with methods PlaceAnOrder, ProceedToPayment and CalculateShipment .

Now create a class named “InternationalOrder” and inherit “IOrderPlacement” with it

public class InternationalOrder : IOrderPlacement
{
    public void PlaceAnOrder()
    {
        //code here for order placement
    }

    public void ProceedToPayment()
    {
        //code here to proceed to payment
    }

    public void CalculateShipment()
    {
        //code here to calculate shipment
    }
}        

There will two types of orders Local and International.

We created a LocalOrder class and inherited IOrderPlacement with it.

public class LocalOrder : IOrderPlacement
{
    public void PlaceAnOrder()
    {
        //code here for order placement
    }

    public void ProceedToPayment()
    {
        //code here to proceed to payment
    }

    public void CalculateShipment()
    {
        //method not required incase of local order
    }
}        

Somehow we come to know that we are missing a requirement that shipment will not be deducted incase of local order. But we are forced to implement CalculateShipment in LocalOrder class and this is the violation of Interface Segregation Principle (ISP)

To resolve this problem by dividing IOrderPlacement interface.


public interface IOrderPlacement
{
    void PlaceAnOrder();
    void ProceedToPayment();
}

public interface InternationalCase
{
    void CalculateShipment();
}

public class LocalOrder : IOrderPlacement
{
    public void PlaceAnOrder()
    {
        //code here for order placement
    }
 
    public void ProceedToPayment()
    {
        //code here to proceed to payment
    }
}

public class InternationalOrder : IOrderPlacement, InternationalCase
{
    public void PlaceAnOrder()
    {
        //code here for order placement
    }

    public void ProceedToPayment()
    {
        //code here to proceed to payment
    }

    public void CalculateShipment()
    {
        //code here to calculate shipment
    }
}        


Dependency Inversion Principle

This principle is about dependencies between the components. The definition of this principle is given by Robert C. Martin:

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

 In other words high-level modules should depend on abstraction, not on the details, of low-level modules means there should not be a tight coupling among components of software and to avoid that, the components should depend on abstraction.

Inversion of Control (IoC)

IoC is a technique to implement the Dependency Inversion Principle in C#. it can be implemented using interface. The rule is that the lower level entities should join the contract to a single interface and the higher-level entities will use only entities that are implementing the interface. This technique removes the dependency between the entities.

Code Example:

public interface IElectronicDevices
{
    void SwitchOnDevice();
    void SwitchOffDevice();
}

public class Fan : IElectronicDevices
{   
    public void SwitchOnDevice()
    {
        Console.WriteLine("Fan Switched on");
    }

    public void SwitchOffDevice()
    {
        Console.WriteLine("Fan Switched off");
    }
}

public class WashingMachine : IElectronicDevices
{    
    public void SwitchOnDevice()
    {
        Console.WriteLine("Fan Switched on");
    }

    public void SwitchOffDevice()
    {
        Console.WriteLine("Fan Switched off");
    }
}

public class ElectronicDevicesController
{
    IElectronicDevices m_ElectronicDevices;

    public ElectronicDevicesController(IElectronicDevices electronicDevices)
    {
        this.m_ElectronicDevices = electronicDevices;
    }

    public void SwithOn()
    {
        m_ElectronicDevices.SwitchOnDevice();
    }

    public void SwitchOff()
    {
        m_ElectronicDevices.SwitchOffDevice();
    }
}


class Program
{
    static void Main(string[] args)
    {
        IElectronicDevices device = new Fan();
        //IAutomobile automobile = new SUV();
        ElectronicDevicesController electronicDevicesController = new ElectronicDevicesController(device);
        device.SwitchOnDevice();
        device.SwitchOffDevice();

        Console.Read();
    }
}        

To view or add a comment, sign in

More articles by Sohaib Sarwar

Explore content categories