×

iFour Logo

A Comprehensive Guide on C# SOLID Principles

Kapil Panchal - September 23, 2021

Listening is fun too.

Straighten your back and cherish with coffee - PLAY !

  • play
  • pause
  • pause
A Comprehensive Guide on C# SOLID Principles

Table of Content

In C#, SOLID design principles are basic design principles. SOLID Principles enable us to overcome most of the design problems. In the 1990s, Robert Martin compiled these principles. These principles provided us with many ways to overcome tight coupled code and least encapsulation.

Usually, Developers start the application building process with good and clean designs according to their experience. But, as time passes, applications might develop bugs. The application has to be changed for every new feature or change request. After a long time, we might need to put more effort, even for small tasks and it might require a lot of time and energy. But as they are part of software development, we cannot refuse them as well.

Here are few design issues which are causing damage in any software in most cases:

  1. When we put more stress on specific classes by assigning them more responsibilities to them which are not related to that class.
  2. When we force the class to depend on each other or when they are tightly coupled.
  3. When we use duplicate code in the application

To overcome these issues, we can implement the following tactics:

  1. We must choose the right architecture for the application
  2. We must follow the design principles made by experts.
  3. We must choose the correct design patterns to build the software according to its specifications and needs.

Here, SOLID stands for as shown below:

S = Single Responsibility Principle

O = Open Closed Principle

L = Liskov Substitution Principle

I = Interface Segregation Principle

D = Dependency Inversion Principle

Single Responsibility Principle (SRP):


As per the concepts of the Single Responsibility Principle, every module of the software must have only a single reason to change. Everything in that particular class

This means, that every single class or class-like structure in our code must have only one job to perform. All the things in that class must be related to one single purpose. Our class must not be multifunctional at all.

The Single Responsibility Principle represents a way of recognizing classes at the App’s design phase making you think of all the ways a class can change. When you see an excellent clarity of operation requests, a fine separation of responsibilities occurs.

Let us comprehend this with an example.

 
                  public class UserService
                  {
                    EmailService _emailService;
                    DbContext _dbContext;
                    public UserService(EmailService aEmailService, DbContext aDbContext)
                    {
                      _emailService = aEmailService;
                      _dbContext = aDbContext;
                    }
                    public void Register(string email, string password)
                    {
                      if (!_emailService.ValidateEmail(email))
                      throw new ValidationException("Email is not an email");
                      var user = new User(email, password);
                      _dbContext.Save(user);
                      emailService.SendEmail(new MailMessage("myname@mydomain.com", email) {Subject="Hi. How are you!"});
                    }
                  }
                  public class EmailService
                  {
                    SmtpClient _smtpClient;
                    public EmailService(SmtpClient aSmtpClient)
                    {
                      _smtpClient = aSmtpClient;
                    }
                    public bool virtual ValidateEmail(string email)
                    {
                      return email.Contains("@");
                    }
                    public bool SendEmail(MailMessage message)
                    {
                      _smtpClient.Send(message);
                    }
                  }

Open-Closed Principle (OCP):


The Open/closed principle says "A software module/class is opened for the extension and closed for editing".

Here, "Open for extension" means that we should design our module/class so that the new feature can only be added when new requirements are generated. Here, "Open for extension" means that we should design our module/class so that the new functionality can only be added when new requirements are created. We should never tamper with it until we find some bugs. As mentioned, a class should be open for extensions, we can use inheritance for this. OK, let's take one example.

 
public class Rectangle: Shape
                  {
                    public double Height {get;set;}
                    public double Width {get;set;}
                    public override double Area()
                    {
                      return Height * Width;
                    }
                  }
                  public class Circle: Shape
                  {
                    public double Radius {get;set;}
                    public override double Area()
                    {
                      return Radius * Radius * Math.PI;
                    }
                  }
                  
                  public class AreaCalculator
                  {
                    public double TotalArea(Shape[] arrShapes)
                    {
                      double area=0;
                      foreach(var objShape in arrShapes)
                      {
                        area += objShape.Area();
                      }
                      return area;
                    }  
                  }

Now our code tracks SRP and OCP at once. When you introduce a new form deriving from the abstract class "Shape", you do not need to change the class "AreaCalculator".

Liskov Substitution Principle (LSP):


The Liskov Substitution Principle (LSP) states that "you should be able to use any derived class instead of a parent class and have it behave in the same manner without modification". It guarantees that a derived class does not affect the behavior of the parent class, in other words, that a derived class must be substitutable for its basic class.

This principle is just an extension of the Open-Closed Principle and it means that we need to make sure that the new derived classes extend the basic classes without changing their behavior. I will explain it by employing a concrete example that violates the LSP.

A dad is a doctor and his son wants to be a cricket player. So here the son cannot replace his father even if the two belong to the same family hierarchy.

 
   public class SqlFileManager
{
  public string GetTextFromFiles(List aLstReadableFiles)
  {
    StringBuilder objStrBuilder = new StringBuilder();
    foreach(var objFile in aLstReadableFiles)
    {
      objStrBuilder.Append(objFile.LoadText());
    }
    return objStrBuilder.ToString();
  }
  public void SaveTextIntoFiles(List aLstWritableFiles)
  {
    foreach(var objFile in aLstWritableFiles)
    {
      objFile.SaveText();
    }
  }
}

              

Looking for Trusted C# Development Company ?
For Your Business?

Interface Segregation Principle (ISP):


The principle of segregation of interfaces stipulates that customers should not be forced to implement interfaces they do not use. Rather than a fat interface, many small interfaces are preferred depending on method groups, each serving as a sub-module.

It may be defined differently. An interface should be closer to the code which uses it than to the code which implements it. So the methods of the interface are defined by the methods that the client code needs instead of the methods that the class implements. As a result, clients should not be forced to depend on interfaces they do not use.

As with classes, each interface needs to have a specific purpose/responsibility (see SRP). You should not have to implement an interface when your object does not share this goal. The larger the interface, the more likely it is that it includes methods that not every implementer can do. That's the essence of the Interface Segregation Principle. Let's start with an example.

 
                public class TeamLead: IProgrammer, ILead
                {
                  public void AssignTask()
                  {
                    //Code to assign a Task
                  }
                  public void CreateSubTask()
                  {
                    //Code to create a sub-task from a task.
                  }
                  public void WorkOnTask()
                  {
                    //code to implement to work on the Task.
                  }
                }

              

Dependency Inversion Principle (DIP):


The Dependency Inversion Principle says that all the high-level modules and classes must not depend on low-level modules and classes. All must depend upon abstractions. Also, abstractions must not depend upon details of it, instead, they should depend upon abstractions.

 
  public class DataExporter
{
  public void ExportDataFromFile()
  {
    ExceptionLogger _exceptionLogger;
    try 
    {
      //code to export data from files to database.
    }
    catch(IOException ex)
    {
      _exceptionLogger = new ExceptionLogger(new DbLogger());
      _exceptionLogger.LogException(ex);
    }
    catch(SqlException ex)
    {
      _exceptionLogger = new ExceptionLogger(new EventLogger());
      _exceptionLogger.LogException(ex);
    }
    catch(Exception ex)
    {
      _exceptionLogger = new ExceptionLogger(new FileLogger());
      _exceptionLogger.LogException(ex);
    }
  }
}
                

Conclusion


In this article, we discussed the C# SOLID principles for better coding standards and how to make it as fruitful as possible. We also saw a few examples to deeply understand these concepts.

 
A Comprehensive Guide on C# SOLID Principles Table of Content 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) 6. Conclusion In C#, SOLID design principles are basic design principles. SOLID Principles enable us to overcome most of the design problems. In the 1990s, Robert Martin compiled these principles. These principles provided us with many ways to overcome tight coupled code and least encapsulation. Usually, Developers start the application building process with good and clean designs according to their experience. But, as time passes, applications might develop bugs. The application has to be changed for every new feature or change request. After a long time, we might need to put more effort, even for small tasks and it might require a lot of time and energy. But as they are part of software development, we cannot refuse them as well. Here are few design issues which are causing damage in any software in most cases: When we put more stress on specific classes by assigning them more responsibilities to them which are not related to that class. When we force the class to depend on each other or when they are tightly coupled. When we use duplicate code in the application To overcome these issues, we can implement the following tactics: We must choose the right architecture for the application We must follow the design principles made by experts. We must choose the correct design patterns to build the software according to its specifications and needs. Here, SOLID stands for as shown below: S = Single Responsibility Principle O = Open Closed Principle L = Liskov Substitution Principle I = Interface Segregation Principle D = Dependency Inversion Principle Single Responsibility Principle (SRP): As per the concepts of the Single Responsibility Principle, every module of the software must have only a single reason to change. Everything in that particular class This means, that every single class or class-like structure in our code must have only one job to perform. All the things in that class must be related to one single purpose. Our class must not be multifunctional at all. The Single Responsibility Principle represents a way of recognizing classes at the App’s design phase making you think of all the ways a class can change. When you see an excellent clarity of operation requests, a fine separation of responsibilities occurs. Let us comprehend this with an example.   public class UserService { EmailService _emailService; DbContext _dbContext; public UserService(EmailService aEmailService, DbContext aDbContext) { _emailService = aEmailService; _dbContext = aDbContext; } public void Register(string email, string password) { if (!_emailService.ValidateEmail(email)) throw new ValidationException("Email is not an email"); var user = new User(email, password); _dbContext.Save(user); emailService.SendEmail(new MailMessage("myname@mydomain.com", email) {Subject="Hi. How are you!"}); } } public class EmailService { SmtpClient _smtpClient; public EmailService(SmtpClient aSmtpClient) { _smtpClient = aSmtpClient; } public bool virtual ValidateEmail(string email) { return email.Contains("@"); } public bool SendEmail(MailMessage message) { _smtpClient.Send(message); } } Read More: Detailed Overview Of Design Patterns In C-sharp Open-Closed Principle (OCP): The Open/closed principle says "A software module/class is opened for the extension and closed for editing". Here, "Open for extension" means that we should design our module/class so that the new feature can only be added when new requirements are generated. Here, "Open for extension" means that we should design our module/class so that the new functionality can only be added when new requirements are created. We should never tamper with it until we find some bugs. As mentioned, a class should be open for extensions, we can use inheritance for this. OK, let's take one example.   public class Rectangle: Shape { public double Height {get;set;} public double Width {get;set;} public override double Area() { return Height * Width; } } public class Circle: Shape { public double Radius {get;set;} public override double Area() { return Radius * Radius * Math.PI; } } public class AreaCalculator { public double TotalArea(Shape[] arrShapes) { double area=0; foreach(var objShape in arrShapes) { area += objShape.Area(); } return area; } } Now our code tracks SRP and OCP at once. When you introduce a new form deriving from the abstract class "Shape", you do not need to change the class "AreaCalculator". Liskov Substitution Principle (LSP): The Liskov Substitution Principle (LSP) states that "you should be able to use any derived class instead of a parent class and have it behave in the same manner without modification". It guarantees that a derived class does not affect the behavior of the parent class, in other words, that a derived class must be substitutable for its basic class. This principle is just an extension of the Open-Closed Principle and it means that we need to make sure that the new derived classes extend the basic classes without changing their behavior. I will explain it by employing a concrete example that violates the LSP. A dad is a doctor and his son wants to be a cricket player. So here the son cannot replace his father even if the two belong to the same family hierarchy.   public class SqlFileManager { public string GetTextFromFiles(List aLstReadableFiles) { StringBuilder objStrBuilder = new StringBuilder(); foreach(var objFile in aLstReadableFiles) { objStrBuilder.Append(objFile.LoadText()); } return objStrBuilder.ToString(); } public void SaveTextIntoFiles(List aLstWritableFiles) { foreach(var objFile in aLstWritableFiles) { objFile.SaveText(); } } } Looking for Trusted C# Development Company ? For Your Business? CONNECT US Interface Segregation Principle (ISP): The principle of segregation of interfaces stipulates that customers should not be forced to implement interfaces they do not use. Rather than a fat interface, many small interfaces are preferred depending on method groups, each serving as a sub-module. It may be defined differently. An interface should be closer to the code which uses it than to the code which implements it. So the methods of the interface are defined by the methods that the client code needs instead of the methods that the class implements. As a result, clients should not be forced to depend on interfaces they do not use. As with classes, each interface needs to have a specific purpose/responsibility (see SRP). You should not have to implement an interface when your object does not share this goal. The larger the interface, the more likely it is that it includes methods that not every implementer can do. That's the essence of the Interface Segregation Principle. Let's start with an example.   public class TeamLead: IProgrammer, ILead { public void AssignTask() { //Code to assign a Task } public void CreateSubTask() { //Code to create a sub-task from a task. } public void WorkOnTask() { //code to implement to work on the Task. } } Dependency Inversion Principle (DIP): The Dependency Inversion Principle says that all the high-level modules and classes must not depend on low-level modules and classes. All must depend upon abstractions. Also, abstractions must not depend upon details of it, instead, they should depend upon abstractions.   public class DataExporter { public void ExportDataFromFile() { ExceptionLogger _exceptionLogger; try { //code to export data from files to database. } catch(IOException ex) { _exceptionLogger = new ExceptionLogger(new DbLogger()); _exceptionLogger.LogException(ex); } catch(SqlException ex) { _exceptionLogger = new ExceptionLogger(new EventLogger()); _exceptionLogger.LogException(ex); } catch(Exception ex) { _exceptionLogger = new ExceptionLogger(new FileLogger()); _exceptionLogger.LogException(ex); } } } Conclusion In this article, we discussed the C# SOLID principles for better coding standards and how to make it as fruitful as possible. We also saw a few examples to deeply understand these concepts.  

Build Your Agile Team

Enter your e-mail address Please enter valid e-mail

Categories

Ensure your sustainable growth with our team

Talk to our experts
Sustainable
Sustainable
 

Blog Our insights

Power Apps vs Power Automate: When to Use What?
Power Apps vs Power Automate: When to Use What?

I often see people asking questions like “Is Power App the same as Power Automate?”. “Are they interchangeable or have their own purpose?”. We first need to clear up this confusion...

Azure DevOps Pipeline Deployment for Competitive Business: The Winning Formula
Azure DevOps Pipeline Deployment for Competitive Business: The Winning Formula

We always hear about how important it is to be competitive and stand out in the market. But as an entrepreneur, how would you truly set your business apart? Is there any way to do...

React 18 Vs React 19: Key Differences To Know For 2024
React 18 Vs React 19: Key Differences To Know For 2024

Ever wondered how a simple technology can spark a revolution in the IT business? Just look at React.js - a leading Front-end JS library released in 2013, has made it possible. Praised for its seamless features, React.js has altered the way of bespoke app development with its latest versions released periodically. React.js is known for building interactive user interfaces and has been evolving rapidly to meet the demands of modern web development. Thus, businesses lean to hire dedicated React.js developers for their projects. React.js 19 is the latest version released and people are loving its amazing features impelling them for its adoption.