SOLID Design Principles Explained - C#

Sachin Gandhi  Print   11 min read  
14 Dec 2018
18 Sep 2019
Beginner
30.4K

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.

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 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 more changes requested in the future, the more changes the class need to apply.

Understanding

Single Responsibility Principle is one of the five SOLID principles which 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 not 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 its working as per logic defined. And it’s easy to change in class as it has 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 number of bugs and improves development speed and most importantly makes developer’s life 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. Below example violates SRP principle. The code below, violates 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 interface. A new interface called IGarageUtility is created and gate related methods are moved to 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 saving, corporate etc. for different customers. As for each customer type, there are different rules and different interest rates. The code below violates 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 you want to extend functionality. Here I have used 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 interface, we can extend functionality.

Above code is implementing both OCP and SRP principle, as each class has single is doing a single task and we are not modifying 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 program or module is using base class then derived class should be able to extend their 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 printing shape of a triangle, with 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 above implementation, we need to refactor this code by introducing interface with 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: Fruit
 {
 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 which 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

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 class will results in having sharing methods which are not relevant to derived classes but its there in the base class so that 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 below code, ISP is broken as process method is not required by 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 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 as 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 invert 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 below implementation, I have used interface as a reference, but you can use abstract class or interface as per your requirement.

Implementation

In below code, we have implemented DIP using IoC using injection constructor. There are different ways to implement Dependency injection. Here, I have use injection thru constructor but you inject the dependency into class's constructor (Constructor Injection), set property (Setter Injection), method (Method Injection), events, index properties, fields and basically 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, IAutomobile interface is in an abstraction layer and AutomobileController as the higher-level module. Here, we have integrated all in a single code but in real-world, each abstraction layer is a separate class with additional functionality. Here products are completely decoupled from the consumer using IAutomobile interface. The object is injected into the constructor of the AutomobileController class in reference to the interface IAutomobile. The constructor where the object gets injected is called injection constructor.

DI is a software design pattern that allows us to develop loosely coupled code. Using DI, we can reduce tight coupling between software components. DI also allows us to better accomplish future changes and other difficulties in our software. The purpose of DI is to make code sustainable.

Read More Articles Related to Design patterns
Conclusion

The five solid principles discussed above is good for Object Oriented design. Most of the principles involved are adding a layer of abstraction between classes that would otherwise dependent on each other, thus creating a loose coupling relationship which results in less rigid and fragile code. It is always recommended to keep these principles in mind when writing new code.

Take our free skill tests to evaluate your skill!

In less than 5 minutes, with our skill test, you can identify your knowledge gaps and strengths.

Learn to Crack Your Technical Interview

+
+
Accept cookies and close this message