The SOLID principles are a standard software development practice that should be followed in all software applications that use Object-Oriented Programming. They set the norm of how to write a program with clean architecture. SOLID principles in C# help to write code that is easy to test, easy to maintain and easy to extend.
In this article, we will see essential information on SOLID principles, and we will see the major advantages of using SOLID Principles in C# applications.
So, the main goal of the SOLID principle is to Develop a flexible, testable and maintainable application. It consists of 5 principles.
5 SOLID Principles in C#
In this article, we’ll bring a profound plunge into every one of these 5 principles and clarify how they work with C# code. Don’t worry, if the concept is clear, then you can apply it with other programming languages that suit you well.
S (Single Responsibility)
The very first principle says that a class should be created for a single specific purpose. It means a class should not be responsible for multiple tasks.
In other words, we can say that if a class is presented with higher cohesion then it is said that it follows the Single Responsibility Principle.
Cohesion shows the relation of software components by which they are related.
A very basic example to understand this principle is the User Registration process. In the UserRegisteration Service class, if we write code that sends welcome messages or credential details, this violates the Single Responsibility Principle.
In the below code snippet, Registration and Login methods are related but SendEmail()
and SendSMS()
methods are not related to Login or Registration.
So, In the below C# code snippet, we can say that they have a low level of cohesion and do not follow the Single Responsibility Principle.
public class Userservice
{
public void UserRegisteration()
{
//Do the Registration Process
}
public bool Login(string userName, string password)
{
try
{
return true;
}
catch
{
return false;
}
}
public void SendEmail()
{
//Send Mail to newly registered users
}
public void SendSMS()
{
//Send SMS to newly registered users
}
}
Instead, create a different class for Email and Send SMS Service and then after successful registration, we may call a function written in Email Service to send an email and SMS to users.
Now, the Registration and Login processes are in a single class while SendEmail()
and SendSMS()
methods are in different classes. In this way, they present a high level of cohesion and now follow the Single Responsibility Principle.
public class Userservice
{
public void UserRegisteration()
{
//Do the Registration Process
}
public bool Login(string userName, string password)
{
try
{
return true;
}
catch
{
return false;
}
}
}
public class MailerService
{
public void SendEmail()
{
//Send Mail to newly registered users
}
public void SendSMS()
{
//Send SMS to newly registered users
}
}
Note: A high level of cohesion helps to achieve the Single Responsibility Principle.
O (Open Close Principle)
This principle says that a class or function should be open for extension but should be closed for modification. This is one of the very important SOLID Principles in C#.
Consider the Below example –
In the below code snippet, suppose a new docType is required then this class needs a modification and we need to add one more condition to achieve this. This violates the Open Close Principle.
public class ExportDocument
{
public void Export(string docType)
{
if (docType == "pdf")
{
//Export Document in PDF Format
}
else if (docType == "word")
{
//Export Document in Word Format
}
else if (docType == "excel")
{
//Export Document in Excel Format
}
}
}
To overcome this issue, we can create an interface.
public interface IExportDocument
{
public void Export();
}
Next, create classes as per document type to export and inherit them from the interface as created above.
public class ExportDocumentInPDFFormat : IExportDocument
{
public void Export()
{
//Export Document in PDF format
}
}
public class ExportDocumentInWordFormat : IExportDocument
{
public void Export()
{
//Export Document in Word format
}
}
public class ExportDocumentInExcelFormat : IExportDocument
{
public void Export()
{
//Export Document in Excel format
}
}
In case a new document type is required to export then simply create a class as shown below and inherit it from IExportDocument. In this way, none of the existing classes is required to modify and we follow the Open Close Principle.
public class ExportDocumentInCSVFormat : IExportDocument
{
public void Export()
{
//Export Document in CSV format
}
}
We can use an abstract class instead of an interface to achieve this principle.
L (Liskov substitution Principle)
If S1 is a subtype of T1 then the object of S1 may replace the object of T1. It means derived types can be substituted for the base types.
This principle can be achieved with the help of an interface or abstract class. We will use an abstract class.
public abstract class Car
{
public abstract string GetCarModel();
}
public class Audi : Car
{
public override string GetCarModel()
{
return "A4";
}
}
public class BMW : Car
{
public override string GetCarModel()
{
return "X5";
}
}
Now, with a car object, we will call methods of Audi and BMW classes as Car is the parent class.
static void Main(string[] args)
{
Car car = new Audi();
Console.WriteLine(car.GetCarModel());
car = new BMW();
Console.WriteLine(car.GetCarModel());
}
I (Interface Segregation Principle)
The Interface Segregation Principle says that there should be multiple Interfaces instead of a single interface. This will not force the client to implement interfaces they do not require.
Now, consider the below example –
There is one interface that contains 2 methods, the first method is to export and format the document and the second method is to send that document over email.
interface ExportDocumentFormat
{
void DocumentFormatter();
void SendDocumentMail();
}
Suppose, we have one class which do document export/ formatting and send an email. This is fine.
public class ExportDcoumentAndSendMail : ExportDocumentFormat
{
public void DocumentFormatter()
{
//Export and Format Document
}
public void SendDocumentMail()
{
//Send Email
}
}
Next, we have one more class that does only one task i.e. document formatting. This class doesn’t require Email related functionality. However, we are forced to implement this as this class inherits an interface that contains both methods. In this case, we are violating the principle “Interface Segregation Principle”
public class ExportDcoumentOnly : ExportDocumentFormat
{
public void DocumentFormatter()
{
//Export and Format Document
}
public void SendDocumentMail()
{
//Send Email
}
}
Now, to follow this principle, we will segregate the interface into 2 parts. You can do this based on your requirements.
So, Let’s refactor the above code to follow the Interface Segregation Principle.
Here, one interface segregated into 2
interface ExportDocumentFormat
{
void DocumentFormatter();
}
interface SendDocumentOverMail
{
void SendDocumentMail();
}
For the class which needs both methods, we will implement it with both interfaces (Multiple inheritances) and for the class which needs only one method then we can inherit this accordingly.
public class ExportDcoumentAndSendMail : ExportDocumentFormat, SendDocumentOverMail
{
public void DocumentFormatter()
{
//Export and Format Document
}
public void SendDocumentMail()
{
//Send Email
}
}
public class ExportDcoumentOnly : ExportDocumentFormat
{
public void DocumentFormatter()
{
//Export and Format Document
}
}
D (Dependency Inversion Principle)
This SOLID design principle says that a high-level module shouldn’t depend upon a low-level module. It avoids tight coupling and helps us to develop an application whose code is testable, and maintainable.
Let’s understand the benefit of the Dependency Inversion Principle with the below code snippet.
We will create an interface and call it IDocumentPrinting
public interface IDocumentPrinting
{
void PDFPrinting();
void MSWordPrinting();
}
Next, create one class that inherits the IDocumentPrinting
interface and implemented in ProjectModule1
class.
public class ProjectModule1 : IDocumentPrinting
{
public void PDFPrinting()
{
Console.WriteLine("PDF Printing for Module1");
}
public void MSWordPrinting()
{
Console.WriteLine("MS-Word Printing for Module1");
}
}
Next, create a controller class that calls the methods of the IDocumentPrinting
interface.
public class FileFormatController
{
IDocumentPrinting _documentPrinting;
public FileFormatController(IDocumentPrinting documentPrinting)
{
this._documentPrinting = documentPrinting;
}
public void PDFFile()
{
_documentPrinting.PDFPrinting();
}
public void WordFile()
{
_documentPrinting.MSWordPrinting();
}
}
Finally, call the PDFPrinting()
and MSWordPrinting()
method as shown in the below code snippet.
static void Main(string[] args)
{
IDocumentPrinting documentPrinting = new ProjectModule1();
FileFormatController fileController = new FileFormatController(documentPrinting);
documentPrinting.PDFPrinting();
documentPrinting.MSWordPrinting();
Console.Read();
}
We are yet to implement the benefit of the Dependency Injection Principle.
Let’s assume that due to some scenarios we have to create another module that will have similar methods but there will be some differences. Now, the real game starts.
We will just create a new class ProjectModule2 and will inherit with the same interface i.e. IDocumentPrinting
public class ProjectModule2 : IDocumentPrinting
{
public void PDFPrinting()
{
Console.WriteLine("PDF Printing for Module2");
}
public void MSWordPrinting()
{
Console.WriteLine("MS-Word Printing for Module2");
}
}
In the main method, just use the below code snippet. You may notice that instead of ProjectModule1 I have used ProjectModule2 and it calls the method of the newly created class.
static void Main(string[] args)
{
IDocumentPrinting documentPrinting = new ProjectModule2();
FileFormatController fileController = new FileFormatController(documentPrinting);
documentPrinting.PDFPrinting();
documentPrinting.MSWordPrinting();
Console.Read();
}
So, in this way, we have refactored our source code to maintain the Dependency Inversion Principle. DI Principle is mainly focused on code maintainability which helps the developer with future requirements and related changes.
Benefits of using SOLID Principles in C#
Now, that we have covered the 5 SOLID Design principles, let’s explore the benefits of using SOLID Design Principles in C# code.
- Code Maintainability – Code maintenance is one of the most important tasks in the software development field. If the code is well maintained then it means that you are a successful software engineer or architect. As the industry progresses, the software also changes according to the need. Therefore, you should design the code of your software in such a way that you can easily adapt to future changes. SOLID Principles help you to achieve this.
- Testability – The testing phase plays an important role in making software successful. So, you should design your application in such a manner that one can test every functionality.
- Parallel Development – Team members are needed for the software to be successful, so the design should be such that different team members can work independently on different modules. This will also help to reduce overall software completion time.
- Extensibility – A good software should be easy to extend. It means developers or engineers can enhance the existing software with minimum modification and without affecting running software.
Given above are 4 major factors that help in developing successful, stable and long-running software. If you will use SOLID Principles properly then only you will be able to develop good software.
Summary:
SOLID Principles helps to write better code in the C# programming language. All the approach given in SOLID principles has their significant advantages. The main purpose of the SOLID principle is to minimize the impact of changes in the existing software applications.
SOLID Principles consists of 5 principles –
- S – Single Responsibility– It tells that a class should not be assigned many jobs to do.
- O- Open Close Principle – This principle says a class or module should be open for extension but should be closed for modification.
- L – Liskov Substitution Principle – This principle implies that derived types can be substituted for the base types.
- I – Interface Segregation Principle – This part of the SOLID principle says not to overload the interface with too many methods. Try to use multiple interfaces within your application instead of one fat interface.
- D – Dependency Inversion Principle (DIP) – It avoids tight coupling and helps us to develop an application whose code is testable, and maintainable.