Month End Sale: Get Extra 10% OFF on Job-oriented Training! Offer Ending in
D
H
M
S
Get Now
SOLID Principles In C# Explained

SOLID Principles In C# Explained

26 Jun 2024
Beginner
493K Views
23 min read
Learn via Video Course & by Doing Hands-on Labs

⭐ .NET Design Patterns Course: Design Patterns in C# Online Training

SOLID Principles Explained Using C#: An Overview

In Object-Oriented Programming (OOP), SOLID is an acronym introduced by Michael Feathers for five design principles used to make software design more understandable, flexible, and maintainable. These principles are a subset of many promoted by Robert C. Martin

If you're interested in learning more about these principles and how to apply them in your software development projects, you may find a comprehensive guide in our Design Pattern Tutorial. Also, check out .NET Design Patterns Training Course to explore Design patterns with real-world examples.

What are SOLID Design Principles?

The SOLID Principles C# manages the majority of software design problems that developers face daily. These concepts are tried-and-true processes that make software designs clearer, more flexible, and more maintainable. As a result, if we follow these rules when creating our applications, we can create superior applications.

What are the SOLID principles C#?

There are five SOLID principles in C#:
  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)

1. Single Responsibility Principle (SRP)

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

Robert C. Martin gave this definition in his book Agile Software Development, Principles, Patterns, and Practices, which waslater republished in the C# version of the book Agile Principles, Patterns, and Practices in C#.

In layman's terminology, this means that a class should not be loaded with multiple responsibilities, and a single responsibility should not be spread across multiple classes or mixed with other responsibilities. The reason is that the more changes requested in the future, the more changes the class needs to apply.

Understanding

  • The single Responsibility Principle is one of the five SOLID principles in C# that developers use to guide them as they write code or design an application.
  • In simple terms, a module or class should have a very small piece of responsibility in the entire application. As the rule states, a class/module should have no more than one reason to change.
  • If a class has only a single responsibility, it is likely to be very robust. It’s easy to verify that it's working according to the logic defined, and it’s easy to change in class as it has a single responsibility.
  • The Single Responsibility Principle provides another benefit. Classes, software components, and modules that have only one responsibility are much easier to explain, implement, and understand than ones that give a solution for everything.
  • This also reduces the number of bugs, improves development speed, and, most importantly, makes the developer’s life a lot easier.

Implementation

Let’s take a scenario of Garage service station functionality. It has three main functions: open gate, close gate, and performing service. The below example violates the SRP principle. 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 functinality
 }
 
 public void PerformService(Vehicle vehicle)
 {
 //Check if garage is opened
 //finish the vehicle service
 }
 
 public void DoCloseGate()
 {
 //Close the gate functinality
 }
}

We can correctly apply SRP by refactoring the above code and introducing an interface. A new interface called IGarageUtility is created, and gate-related methods are moved to a different class called GarageStationUtility.

public class GarageStation
{
 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();
 }
}
 public class GarageStationUtility : IGarageUtility
{
 public void OpenGate()
 {
 //Open the Garage for service
 }
 
 public void CloseGate()
 {
 //Close the Garage functionlity
 }
}
 
public interface IGarageUtility
{
 void OpenGate();
 void CloseGate();
}

2. Open Closed Principle (OCP)

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

Bertrand Meyer is generally credited for having originated the definition of open/closed principle in his book Object-Oriented Software Construction.

Understanding

  • This principle suggests that the class should be easily extended, but there is no need to change its core implementations.
  • The application or software should be flexible to change. How change management is implemented in a system has a significant impact on the success of that application/ software. The OCP states that the behaviors of the system can be extended without having to modify its existing implementation.
  • i.e., New features should be implemented using the new code, not by changing existing code. Adhering to OCP potentially streamlines code maintenance and reduces the risk of breaking the existing implementation.

Implementation

Let’s take an example of bank accounts like regular savings, salary savings, corporate, etc., for different customers. For each customer type, there are different rules and interest rates. The code below violates the OCP principle if the bank introduces a new Account type. Said code modifies this method for adding 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;
 }
}

When we want to extend functionality, we can apply OCP by using interfaces, abstract classes, abstract methods, and virtual methods. Here, I 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;
 }
}

In the above code, three new classes are created: regular saving account, SalarySavingAccount, and CorporateAccount, by extending them from IAccount.This solves the problem of class modification, and by extending the interface, we can extend functionality. The above code implements both the OCP and SRP principles, as each class is doing a single task, and we are not modifying the class and only doing an extension.

3. Liskov Substitution Principle (LSP)

Definition by Robert C. Martin: Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

The Liskov substitution principle (LSP) is a definition of a subtyping relation called (strong) behavioral subtyping. Barbara Liskov initially introduced in a 1987 conference keynote address titled Data Abstraction and Hierarchy.

Understanding

  • LSP states that the child class should be perfectly substitutable for their parent class. If class C is derived from P then C should be substitutable for P.
  • We can check using LSP that inheritance is applied correctly or not in our code.
  • LSP is a fundamental principle of SOLID Principles in C# and states that if a program or module is using a base class, then the derived class should be able to extend its base class without changing its original implementation.

Implementation

Let’s consider the code below where LSP is violated. We cannot simply substitute a Triangle, which results in the printing shape of a triangle, with a Circle.

namespace Demo
{
 public class Program
 {
 static void Main(string[] args)
 {
 Triangle triangle = new Circle();
 Console.WriteLine(triangle.GetColor());
 }
 }
 
 public class Triangle
 {
 public virtual string GetShape()
 {
 return "Triangle";
 }
 }
 
 public class Circle: Triangle
 {
 public override string GetShape()
 {
 return "Circle";
 }
 }
}

To correct the above implementation, we need to refactor this code by introducing an interface with a method called GetShape.

namespace Demo
{
 class Program
 {
 static void Main(string[] args)
 {
 Shape shape = new Circle();
 Console.WriteLine(shape.GetShape());
 shape = new Triangle ();
 Console.WriteLine(shape.GetShape());
 }
 }
 
 public abstract class Shape
 {
 public abstract string GetShape();
 }
 
 public class Triangle: Shape
 {
 public override string GetShape()
 {
 return "Triangle";
 }
 }
 
 public class Circle: Triangle
 {
 public override string GetShape()
 {
 return "Circle";
 }
 }
}

4. Interface Segregation Principle (ISP)

Definition: No client should be forced to implement methods that they do not use, and the contracts should be broken down into thin ones.

The ISP was first used and formulated by Robert C. Martin while consulting for Xerox.

Understanding

  • The interface segregation principle is required to solve the application's design problem.
  • When all the tasks are done by a single class or, in other words, one class is used in almost all the application classes, then it has become a fat class with overburden.
  • Inheriting such a class will result in sharing methods that are not relevant to derived classes, but since they are present in the base class, they will be inherited in the derived class.
  • Using ISP, we can create separate interfaces for each operation or requirement rather than having a single class to do the same work.

Implementation

In the code below, ISP is broken as the process method is not required by the OfflineOrder class but is forced to be implemented.

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
 }
 }

5. Dependency Inversion Principle (DIP)

This principle is about dependencies among components. The definition of DIP is given by Robert C. Martin is as follows:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.

  2. Abstractions should not depend on details. Details should depend on abstractions.

Understanding

  • The principle says that high-level modules should depend on abstraction, not on the details, of low-level modules.
  • In simple words, the principle states that software components should not be tightly coupled and should depend on abstraction to avoid that.
  • The terms Dependency Injection (DI) and Inversion of Control (IoC) are generally used interchangeably to express the same design pattern.
  • The pattern was initially called IoC, but Martin Fowler (known for designing enterprise software) anticipated the name DI because all frameworks or runtimes inverted control in some way, and he wanted to know which aspect of control was being inverted.
  • Inversion of Control (IoC) is a technique to implement the Dependency Inversion Principle in C#.
  • Inversion of control can be implemented using either an abstract class or 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 implement the interface.
  • This technique removes the dependency between the entities.

Note: In the implementation below, I have used an interface as a reference, but you can use an abstract class or interface as per your requirement.

Implementation

In the code below, we have implemented DIP using IoC and an injection constructor. There are different ways to implement Dependency injection. Here, I have used injection through the constructor, but you inject the dependency into the class's constructor (Constructor Injection), set property (Setter Injection), method (Method Injection), events, index properties, fields, and any public members of the class.

public interface IAutomobile
{
 void Ignition();
 void Stop();
}

public class Jeep : IAutomobile
{
 #region IAutomobile Members
 public void Ignition()
 {
 Console.WriteLine("Jeep start");
 }
 
 public void Stop()
 {
 Console.WriteLine("Jeep stopped.");
 }
 #endregion
}
 
public class SUV : IAutomobile
{
 #region IAutomobile Members
 public void Ignition()
 {
 Console.WriteLine("SUV start");
 }
 
 public void Stop()
 {
 Console.WriteLine("SUV stopped.");
 }
 #endregion
}


public class AutomobileController
{
 IAutomobile m_Automobile;
 
 public AutomobileController(IAutomobile automobile)
 {
 this.m_Automobile = automobile;
 }
 
 public void Ignition()
 {
 m_Automobile.Ignition();
 }
 
 public void Stop()
 {
 m_Automobile.Stop();
 }
}
 
class Program
{
 static void Main(string[] args)
 {
 IAutomobile automobile = new Jeep();
 //IAutomobile automobile = new SUV();
 AutomobileController automobileController = new AutomobileController(automobile);
 automobile.Ignition();
 automobile.Stop();
 
 Console.Read();
 }
}

In the above code, the IAutomobile interface is in an abstraction layer, and AutomobileController is the higher-level module. Here, we have integrated everything in a single line of code, but in the real world, each abstraction layer is a separate class with additional functionality. Here, products are completely decoupled from the consumer using the IAutomobile interface. The object is injected into the constructor of the AutomobileController class about the interface automobile. The constructor where the object gets injected is called the injection constructor.

Summary

The SOLID principles C# provide an effective foundation for creating clean, manageable, and extensible object-oriented code in C#. By following these five principles, developers can construct code that is easier to understand, edit, and extend: single responsibility, open/closed, Liskov substitution, interface segregation, and dependency inversion. To learn about various other aspects of design patterns, consider enrolling in our .NET Design Patterns Course.

FAQs

Q1. What is the purpose of SOLID principles in C#?

The SOLID principles in C# seek to improve code maintainability, scalability, and flexibility by encouraging good design practices and decreasing dependencies.

Q2. What is the SOLID principle in MVC?

MVC follows the "S" of the SOLID principles by separating responsibilities. The model includes state information. The view includes items that interact with the user. The controller ensures that the sequence of steps is followed correctly.

Q3. What are the SOLID principles every developer must know?

Every developer should be familiar with the SOLID concepts of single responsibility, open/closed, Liskov substitution, interface segregation, and dependency inversion.

Q4. What is the distinction between SOLID principles and design patterns in C#?

Design patterns are proven solutions for recurring situations. Design principles are recommendations that might assist you in structuring and constructing your software. That's all. You do not have to use every pattern and concept that exists.
Share Article

Live Classes Schedule

Our learn-by-building-project method enables you to build practical/coding experience that sticks. 95% of our learners say they have confidence and remember more when they learn by building real world projects.
Software Architecture and Design Training Jul 28 SAT, SUN
Filling Fast
05:30PM to 07:30PM (IST)
Get Details
ASP.NET Core Certification Training Jul 28 SAT, SUN
Filling Fast
07:00AM to 09:00AM (IST)
Get Details
ASP.NET Core Project Aug 24 SAT, SUN
Filling Fast
07:00AM to 09:00AM (IST)
Get Details

Can't find convenient schedule? Let us know

About Author
Sachin Gandhi (Author and Certified Scrum Master)

He has over 13 years of experience in Microsoft .Net applications development. He comes with expertise in different domains like health care, banking and custom application domains. He has proven skills in cutting-edge technologies and business processes. He has always been interested in learning new technologies and sharing the same with his team members. He has proven track record of managing and leading teams both onsite and offshore. He is always ready to contribute and bring his personal approach Microsoft. the success of the project.

Sachin holds a Masters Degree in Computer Applications from North Gujarat University and a Post Graduate Diploma degree in Financial Management from NMIMS, Mumbai. Furthermore, Sachin is Certified Scrum Master. He is working as Tech Lead with a proven track record in Information technology and service industry. 

Accept cookies & close this