Holi Sale. Get upto 40% OFF on Job-oriented Training! Offer Ending in
D
H
M
S
Get Now
SOLID Design Principles Explained

SOLID Design Principles Explained

19 Jan 2024
Beginner
483K Views
22 min read

SOLID Design 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 principles 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.

What are SOLID Design Principles?

The SOLID Design Principles are used to manage the majority of software design problems that developers face daily. These design concepts are tried and true processes that make software designs more clear, flexible, and maintainable. As a result, if we follow these rules when creating our application, we can create superior applications.

SOLID Principles

There are five SOLID principles:

  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.

Robert C. Martin gave this definition in his book Agile Software Development, Principles, Patterns, and Practices and later 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 that guide developers 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. Or as it 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 it's working as per 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 3 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 of 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 of above code by 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();
}

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, but not by changing existing code. The main benefit of adhering to OCP is that it 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. As for each customer type, there are different rules and different 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;
 }
}

We can apply OCP by using interface, abstract class, abstract methods, and virtual methods when we want to extend functionality. 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 modification of class and by extending the interface, we can extend functionality.The above code implements both the OCP and SRP principles, as each class has single is doing a single task and we are not modifying the class and only doing an extension.

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, that was initially introduced by Barbara Liskov 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 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 their 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";
 }
 }
}

Interface Segregation Principle (ISP)

Definition: No client should be forced to implement methods that it does not use, and the contracts should be broken down to 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 design problem of the application.
  • 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 having sharing methods that are not relevant to derived classes but it's there in the base class so they will inherit 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 below code, ISP is broken as the process method is not required by the OfflineOrder 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)

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 says that there should not be a tight coupling among components of software and to avoid that, the components should depend on abstraction.
  • 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 the enterprise software) anticipated the name as DI because all frameworks or runtime inverted the 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 are implementing the interface.
  • This technique removes the dependency between the entities.

Note:

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

Implementation

In the below code, we have implemented DIP using IoC using 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 members of the class which are public.

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 all in a single 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 IAutomobile. The constructor where the object gets injected is called the injection constructor.

Read More Articles Related to Design patterns
Summary

The SOLID principles provide an effective foundation for creating clean, manageable, and extensible object-oriented code in C#. Developers can construct code that is easier to understand, edit, and extend by following these five principles: single responsibility, open/closed, Liskov substitution, interface segregation, and dependency inversion.

FAQs

Q1. What are the SOLID design principles in C#?

The SOLID acronym represents some wonderful software development concepts, including single responsibility, open for extension and closed for modification, Liskov substitution, interface segregation, and dependency injection.

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 seven design principles?

Design principles include emphasis, alignment, and balance; contrast, repetition; proportion; movement; and white space. The more a designer concentrates on these factors, the better the final design.

Q4. Why use solid design principles?

The SOLID principles of object-oriented programming make object-oriented designs easier to understand, adaptable, and maintain. They also make it simple to write legible and testable code that multiple developers may work on collaboratively from anywhere at any time.

Q5. 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
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