SOLID Design Principles in C#

SOLID principles are used to design the application in such a way that it should be easily maintainable and upgradable. Any software should be written as simple as possible to produce the desired result, however, if the software is not designed properly then it’s a nightmare to maintain and upgrade the software. SOLID principles make it easy for a developer to write easily extendable code and avoid common coding errors. Let’s see SOLID Design Principles in C# in detail.

SOLID Design Principles in C#

Following are the five SOLID Design Principles in C#:

  • S – Single Responsibility Principle (SRP)
  • O – Open-closed Principle (OCP)
  • L – Liskov Substitution Principle (LSP)
  • I – Interface Segregation Principle (ISP)
  • D – Dependency Inversion Principle (DIP)

1. Single Responsibility Principle (SRP)

According to SRP,  A class should take one responsibility and there should be one reason to change that class.

Let’s understand this programmatically. See the following C# example:
public class Student
{
    public int StudentId { get; set; }
    public string Name { get; set; }

    public bool AddStudent(Student student)
    {
        // code to insert student record into db
        return true;
    }
    
    public Student StudentReport(int StudentId)
    {
        // Get the student record based on Studentid
        return student
    }
} 

Do you notice here the Student class is taking following 2 responsibilities:

  1. Add the Student record into the database. Database operation.
  2. Generate the student report. 

Problem: The student class should not take the report generation responsibility because suppose some days after you have the requirement to generate the report into a different format (Ex: Excel format), then the Student class will need to be changed which is a problem.

Solution: According to SRP, one class should take one responsibility hence to overcome this problem we should write another class for report generation functionality. If you make any change in the GenerateReport class will not affect the Student class.

namespace Test_Project
{
    public class GenerateReport
    {        
        public ReportTypes ReportType { get; set; }
        
        public void Report(Student student)
        {
            if (ReportType == ReportTypes.CRYSTAL)
            {
                //code to generate student report in CRYSTAL format
            }
            if (ReportType == ReportTypes.PDF)
            {
                //code to generate student report in PDF format
            }
        }
    }

    public enum ReportTypes
    {
            PDF = 1,
            CRYSTAL = 2
    }
} 


2. Open Closed Principle (OCP)

According to OCP, a class should be open for extension but closed for modification.

Let’s understand this by using the same GenerateReport class example
namespace Test_Project
{
    public class GenerateReport
    {        
        public ReportTypes ReportType { get; set; }
        
        public void Report(Student student)
        {
            if (ReportType == ReportTypes.CRYSTAL)
            {
                //code to generate student report in CRYSTAL format
            }
            if (ReportType == ReportTypes.PDF)
            {
                //code to generate student report in PDF format
            }
        }
    }

    public enum ReportTypes
    {
            PDF = 1,
            CRYSTAL = 2
    }
} 

Problem: Many If clauses are used in the GenerateReport class and if you have a requirement to introduce another new report type like “Excel”, then you need to add another ‘if’ condition. 

Solution: According to OCP, this class should be open for extension but closed for modification. Let’s fix this problem by introducing by following classes.

public class IReport
{
    /// <summary>
    /// Method to generate report
    /// </summary>
    /// <param name="em"></param>
    public virtual void GenerateReport(Student student)
    {
        // From base
    }
}
    
/// <summary>
/// Class to generate Crystal report
/// </summary>
public class CrystalReport : IReport
{
    public override void GenerateReport(Student student)
    {
        // Generate crystal report.
    }
}
/// <summary>
/// Class to generate PDF report
/// </summary>
public class PDFReport : IReport
{
    public override void GenerateReport(Student student)
    {
        // Generate PDF report.
    }
} 
When you introduce a new report type, then just inherit from IReport class. So IReport is open for extension but closed for further modification.
public class ExcelReport: IReport
{
    public override void GenerateReport(Student student)
    {
        // Generate an Excel report.
    }
} 

//Add class diagram

3 Liskov Substitution Principle (LSP)

Third SOLID Design Principles in C# is Liskov substitution principle (LSP)

According to LSP, a derived class should not break the base class’s type definition and behavior that means objects of a base class shall be replaceable with objects of its derived classes without breaking the application. This needs the objects of derived classes to behave in the same way as the objects of your base class.

Let’s understand this principle by taking the same student example. In the following picture, Student is a base class and Senior and Junior Students are the derived classes which are inhering from the base Student class.

//Add class diagram

See the following code:

public abstract class Student
{
    public virtual string GetClassDetails(int studentId)
    {
        //return base class details;
    }
    public virtual string GetStudentDetails(int studentId)
    {
        return base student details;
    }
}
public class SeniorStudent : Student
{
    public override string GetClassDetails(int studentId)
    {
        //return senior student class details;
    }
    
    public override string GetStudentDetails(int studentId)
    {
        //return senior student details;
    }
}
public class JuniorStudent: Student
{
    public override string GetClassDetails(int studentId)
    {
        //return junior student class details;
    }
    
    //We don't have a requirement to store junior student details in the database so we cannot get junior student details.
    public override string GetStudentDetails(int studentId)
    {
        throw new NotImplementedException();
    }
} 
Problem: We don’t have any issue ut to this point, however, following will violate the LSP principle.
List<Student> studentList = new List<Student>();
studentList.Add(new SeniorStudent());
studentList.Add(new JuniorStudent());
foreach (Student student in studentList)
{
    e.GetStudentDetailsDetails(89);
} 

When the above code gets executed it will throw not implemented exception for the junior student which is violating LSP. 

Solution: Divide the entire thing into 2 different interfaces:

  1. IClass 
  2. IStudent 

You can implement these interfaces according to the student type.

public interface IClass
{
    string GetClassDetails(int studentId);
}

public interface IStudent
{
    string GetStudentDetails(int studentId);
} 

Junior Student class will implement only IStudent interface not IClass and the Senior Student class will implement both the interfaces. This will maintain the LSP principle.

4 Interface Segregation Principle (ISP)

According to LSP, any client should not be forced to use an interface that is irrelevant to it. In other words, clients should not be forced to depend on methods that they do not use.


Suppose there is a database for storing data of all types of students (Ex: Senior, Junior), now what will be the best approach for our interface?

public interface IStudent
{
    bool AddStudentDetails();
} 
All types of student’s classes will implement this interface to save data. We are good at this point.
public interface IStudentDatabase
{
    bool AddStudentDetails();
    bool ShowStudentDetails(int studentId);
} 

This is interface will handle student’s database operations (Ex: Add, Display) and all types of students class will implement this interface.

Problem: We are breaking something here. We don’t have a requirement to show junior students details but still, we are forcing junior Student class to show their details from the database. This is breaking ISP.

Solution: Add separate interfaces for each operation.

public interface IAddOperation
{
    bool AddStudentDetails();
}

public interface IGetOperation
{
    bool ShowStudentDetails(int studentId);
} 

Juinor student class will implement only the IAddOperation interface and the Senior student class will implement both the interface. Here, we are not forcing the client (i.e. Junior Student class) to implement irrelevant (i.e. IGetOperation) interface.

//Add interface diagram

5 Dependency Inversion Principle (DIP)

Last and fifth SOLID Design Principles in C# is Dependency Inversion Principle (DIP)

According to DIP, do not write any tightly coupled code because that is a nightmare to maintain when the application is growing bigger and bigger. If a class depends on another class, then we need to change one class if something changes in that dependent class. We should always try to write loosely coupled class.

Let’s understand DIP by taking the following notification system example.

public class Email
{
    public void SendEmail()
    {
        // send mail notification
    }
}

public class Notification
{
    private Email _email;
    public Notification()
    {
        _email = new Email();
    }

    public void PromotionalNotification()
    {
        _email.SendEmail();
    }
} 
Problem: The Notification class depends on the Email class because it only sends one type (i.e. Email) of notification. If we got a requirement to sends SMS notification then we need to change the notification system as well. This is called tightly coupled. We can make this code loosely coupled with the following implementation.
public interface IMessenger
{
    void SendMessage();
}

public class Email: IMessenger
{
    public void SendMessage()
    {
        // code to send email
    }
}

public class SMS : IMessenger
{
    public void SendMessage()
    {
        // code to send SMS
    }
}

public class Notification
{
    private IMessenger _iMessenger;
    public Notification()
    {
        _ iMessenger = new Email();
    }
    public void DoNotify()
    {
        _ iMessenger.SendMessage();
    }
} 

Still, our problem is not fixed as the Notification class depends on Email class. 

Solution: Use dependency injection so that we can make the notification system loosely coupled. 

Dependency Injection (DI) is an object-oriented programming design pattern that allows us to develop loosely coupled code.

Types of Dependency Injections: Following are the 3 types of dependency injections

  1. Constructor Injection
  2. Property Injection
  3. Method Injection

5.1. Constructor Injection

Inject dependency through the client class constructor.
public class Notification
{
    private IMessenger _iMessenger;
    public Notification(Imessenger pMessenger)
    {
        _ iMessenger = pMessenger;
    }
    public void DoNotify()
    {
        _ iMessenger.SendMessage();
    }
} 


5.2. Property Injection

Inject dependency through the public property of a client class.
public class Notification
{
    private IMessenger _iMessenger;

    public Notification()
    {
    }
    public IMessenger MessageService
    {
       private get;
       set
       {
           _ iMessenger = value;
       }
     }

    public void DoNotify()
    {
        _ iMessenger.SendMessage();
    }
} 


5.3. Method Injection

Inject dependency through the public method of a client class.
public class Notification
{
    public void DoNotify(IMessenger pMessenger)
    {
        pMessenger.SendMessage();
    }
} 

DIP helps to write loosely coupled code which is highly maintainable and upgradeable.

Click HERE to learn Dependency Injection in detail.

Conclusion:

This is the basics of SOLID Design Principle in C#. These 5 principles are very helpful to design the large and complex software applications.