This tutorial helps you understand how to use the SOLID Principles in C# code.
SOLID is 5 basic principles, helping to build a good software architecture. You can see that all design patterns are based on these principles. SOLID is assembled from the first 5 abbreviations of these 5 principles:
- S: is single responsibility principle (SRP)
- O: stands for open closed principle (OCP)
- L: Liskov substitution principle (LSP)
- I: interface segregation principle (ISP)
- D: Dependency injection principle (DIP)
Single responsibility principle (SRP)
Each class should only take one responsibility, and there should only be one reason to change. For a more thorough explanation I will take the example. You have a tool that is combined with many different small tools like knives, nail clippers, screwdriver ... Would you like to buy this? I don't think I want to buy.
Because there is a problem with it, if you want to add any tool to it, you need to change its entire structure, which is not good. This is a bad architecture for any system. It's better if nail clippers should only be used for nail clippers, or knives should only be used to cut vegetables.
The following is an example of this principle:
namespace SRP
{
public class Student
{
public int StudentId { get; set; }
public string StudentName { get; set; }
public bool Insert(Studentobj)
{
return true;
}
public void GenerateReport(Student obj)
{
}
}
}
The Student class has two responsibilities, one is the responsibility of manipulating the database and the other is creating the report. The Student class should not undertake the creation of the report because assuming that one day the customer requires to create a report in Excel or any other format, this class must change accordingly. This is not good.
So in order to comply with the SRP, a class should only assume one responsibility, we should write another class for creating the report, so when there are any changes to the report creation, it will not affect to the Student class.
public class ReportGeneration
{
public void GenerateReport(Student obj)
{
}
}
Open closed principle (OCP)
Now we will look at the ReportGeneration class as an example of the second rule. Can you guess the problem in this class?
public class ReportGeneration
{
public string ReportType { get; set; }
public void GenerateReport(Student obj)
{
if (ReportType == "CRS")
{
}
if (ReportType == "PDF")
{
}
}
}
Too many IF statements and if you want to add another type of report such as Excel, you need to write another if. This class should encourage expansion but avoid editing. How to do this?
public class IReportGeneration
{
public virtual void GenerateReport(Student obj)
{
}
}
public class CrystalReportGeneraion : IReportGeneration
{
public override void GenerateReport(Student obj)
{
}
}
public class PDFReportGeneraion : IReportGeneration
{
public override void GenerateReport(Student obj)
{
}
}
If you want to give another report format, you only need to inherit from the interface IReportGeneration. Because IReportGeneration is an interface, it has not implemented the method details, it will help you solve this.
Liskov substitution principle (LSP)
This principle is simple but difficult to understand. Sub-classes should not break the definition and behavior of the parent class. What does this mean? We take an example with Student to help you understand this principle.
public abstract class Student
{
public virtual string GetProjectDetails(int id)
{
return "Base Project";
}
public virtual string GetStudentDetails(int id)
{
return "Base Student";
}
}
public class CasualStudent : Student
{
public override string GetProjectDetails(int id)
{
return "Child Project";
}
public override string GetStudentDetails(int id)
{
return "Child Student";
}
}
public class ContractualStudent : Student
{
public override string GetProjectDetails(int id)
{
return "Child Project";
}
public override string GetStudentDetails(int id)
{
throw new NotImplementedException();
}
}
Do you see it OK? See the paragraph below and it violated this principle:
List<Student> list = new List<Student>();
list.Add(new ContractualStudent());
list.Add(new CasualStudent());
foreach (Student s in list)
{
s.GetStudentDetails(1);
}
Now I guess you understand the problem. Well, with Contractual Student, you will get an exception when the GetStudentDetails(int id) method has not been implemented, and it violates the LSP. So what's the solution? Split them into 2 different interfaces. One is IProject, the other is IStudent and deploy in different types:
public interface IStudent
{
string GetStudentDetails(int id);
}
public interface IProject
{
string GetProjectDetails(int id);
}
Now contractual Student will implement IStudent but no IProject. This helps adhere to the LSP principle.
Interface segregation principle (ISP)
This principle says that any client should not implement an interface that does not suit it. This means, suppose there is a data base to store all types of Students (fixed, temporary), so what is the best approach?
public interface IStudent
{
bool AddStudentDetails();
}
All Student classes will inherit from this interface to store data? Is this okay? Now suppose that any company will tell you that they want to take out only permanent students. What will you do? Add method to interface?
public interface IStudentDatabase
{
bool AddStudentDetails();
bool ShowStudentDetails(int id);
}
But we will break some things. We are focusing on the non-permanent student class to display their details from the database. So the solution is to bring them to another interface.
public interface IAddOperation
{
bool AddStudentDetails();
}
public interface IGetOperation
{
bool ShowStudentDetails(int id);
}
Non-fixed students will only deploy the IAddOperation interface and fixed students will deploy both interfaces
Dependency inversion principle (DIP)
This principle tells you that you should not write code that is tied together because it will be difficult to maintain when the application becomes larger. If a class depends on another class, you will need to change that class if one of the dependent classes has to change. We should try to write classes that are as dependent as possible.
Suppose we have a notification system after saving some information to the database.
public class Email
{
public void SendEmail()
{
}
}
public class Notification
{
private Email _email;
public Notification()
{
_email = new Email();
}
public void PromotionalNotification()
{
_email.SendEmail();
}
}
Now Notification class completely depends on Email class, because it only sends one type of notification. If you want to add a new message like SMS? We also have to change the notification system? It's called tightly coupled. What you can do to help it reduce dependence on each other. OK, see the following example:
public interface IMessenger
{
void SendMessage();
}
public class Email : IMessenger
{
public void SendMessage()
{
}
}
public class SMS : IMessenger
{
public void SendMessage()
{
}
}
public class Notification
{
private IMessenger _iMessenger;
public Notification()
{
_ iMessenger = new Email();
}
public void DoNotify()
{
_ iMessenger.SendMessage();
}
}
Notification class still depends on Email class. But now we use depedency Injection to make them less dependent. There are 3 types of DI, Contructor Injection, Property Injection and Method Injection.
Constructor Injection
public class Notification
{
private IMessenger _iMessenger;
public Notification(Imessenger pMessenger)
{
_ iMessenger = pMessenger;
}
public void DoNotify()
{
_ iMessenger.SendMessage();
}
}
Property Injection
public class Notification
{
private IMessenger _iMessenger;
public Notification()
{
}
public IMessenger MessageService
{
private get;
set
{
_ iMessenger = value;
}
}
public void DoNotify()
{
_ iMessenger.SendMessage();
}
}
Method Injection
public class Notification
{
public void DoNotify(IMessenger pMessenger)
{
pMessenger.SendMessage();
}
}
So SOLID will help us write independent code to reduce dependencies between modules, help improve maintenance efficiency, avoid more risks.