Introduction
I have read many articles about SOLID design
principles and explored with lot of examples to understand better on design principles.
Here, I would like to present with you on SOLID design principle in a simple
and easy way with pictorial and C# codes. Please block 20-30 minutes of your time
to go through and understand on concepts. I am sure that, you will definitely
find the main use of design principles and make use of in your projects.
Why do we have to use design principles?
In the Software Development Life Cycle (SDLC), there are many processes involved, starting from Planning to Deployment of the system. Here, the Development process is a vital process and a longer process to execute, where developers start building an application with good and tidy designs using their knowledge and experience according to business requirement. Later on, the application is qualified and ready to deploy in the production environment. But over time, applications might need some more enhancement with the change request by the client and bug-fixes. We can’t say no to the client to apply the updates, and that are a part of SDLC. The application design must be altered for every change request or new features request. Once requirements are in place, sometimes, we might need to put in a lot of effort, even for simple tasks, and it might require a full working knowledge of the entire system, and this will leads to more time consuming, more effort, more testing and more cost.
The three best solutions -
- Choosing the correct architecture like MVC, 3-tier, MVVM and so on.
- Following the Design Principles (SOLID).
- Choosing the correct Design Patterns to build the software based on its specifications.
What are SOLID Design Principles?
SOLID are five basic design principles which help to create good software. SOLID is an acronym for -
- S = Single responsibility principle (SRP)
- O = Open closed principle (OCP)
- L = Liskov substitution principle (LSP)
- I = Interface segregation principle (ISP)
- D = Dependency inversion principle (DIP)
So let’s start understanding each principle with simple C# examples.
S = Single responsibility principle (SRP)
This principle says “Every software module/class should have only one reason to change”.
Here, I have a picture that represents the Swiss cutter that owns multiple functionalities; that’s great. Don’t you feel this cutter is overloaded with many functionalities?
If one of them needs to be changed, the whole set needs to be disturbed. No fault! I am a great fan of a Swiss cutter. What if I have all as separated items? Then, it looks simple and we have to have no worries if any one of the items is affected.
Let’s see with a C# example.
Consider we have Customer and ReportGeneration classes for the application.
- public class CustomerHelper
- {
- public void AddCustomer(Customer customer)
- {
-
- }
-
- public void UpdateCustomer(Customer customer)
- {
-
- }
-
- public void DeleteCustomer(int id)
- {
-
- }
-
- public Customer GetCustomer(int id)
- {
-
- return new Customer();
- }
-
- public string GenerateReport(Customer customer)
- {
-
- return @"%appdata%\Customer\Report.pdf";
- }
- }
Here, CustomerHelper class is taking two responsibilities. One is to take responsibility for the Customer database operations and another one is to generate Customer report. Customer class should not take the report generation responsibility because some days, after your client asked you to give a facility to generate the report in Excel or any other reporting format, then this class will need to be changed and that is not good.
So, according to SRP, one class should take one responsibility so we should write one different class for report generation so that any change in report generation should not affect the CustomerHelper class.
- public class CustomerHelper
- {
- public void AddCustomer(Customer customer)
- {
-
- }
- . . .
- . . .
- . . .
- public Customer GetCustomer(int id)
- {
-
- return new Customer();
- }
- }
-
-
- public class ReportGenerationHelper
- {
- public string GenerateReport(Customer customer)
- {
-
- return @"%appdata%\Customer\Report.pdf";
- }
- }
O = Open closed principle (OCP)
This principle says “A software module/class is open for extension and closed for modification”.
The above picture represents a hand blender which is an extension for different blending rods and there is no need to change the blending machine.
Let’s see with the Report Generation as we have discussed in SRP.
- public class ReportGenerationHelper
- {
- public string GenerateReport(Customer customer)
- {
-
- return @"%appdata%\Customer\Report.pdf";
- }
- }
Now, the client requests to support the report generation in EXCEL, WORD, and PDF. Here, we do the updates in ReportGenerationHelper class by adding IF condition.
- public class ReportGenerationHelper
- {
- public string ReportType { get; set; }
-
- public string GenerateReport(Customer customer)
- {
- string generatedPath = null;
- if (ReportType == "EXCEL")
- {
-
-
- generatedPath = @"%appdata%\Customer\Report.xls";
- }
- else if (ReportType == "PDF")
- {
-
-
- generatedPath = @"%appdata%\Customer\Report.pdf";
- }
- else if (ReportType == "WORD")
- {
-
-
- generatedPath = @"%appdata%\Customer\Report.docx";
- }
- return generatedPath;
- }
- }
Can you guess what happens if the client requests to add XML report generation, what is the solution? Adding another IF Condition?
That is not a good solution. Here, we are ending with modifying the same class and it is violating SRP again. Then, how we can do it? Here is the solution as below to extend the different formats of the class by inheriting and avoiding the modification in the same class.
- public class ReportGenerationHelper
- {
- public string ReportType { get; set; }
-
- public virtual string GenerateReport(Customer customer)
- {
- string generatedPath = null;
-
-
-
-
- generatedPath = @"%appdata%\Customer\Report.xls";
-
- return generatedPath;
- }
- }
The ‘WordReportGenerationHelper’ class is taking one responsibility. This is only meant for report generation. Now, the client asks you to give a facility to generate the report in 'Word' and this class will do that.
- public class WordReportGenerationHelper : ReportGenerationHelper
- {
- public override string GenerateReport(Customer customer)
- {
- string generatedPath = null;
-
-
-
-
- generatedPath = @"%appdata%\Customer\Report.docx";
-
- return generatedPath;
- }
- }
The ‘PDFReportGenerationHelper’ class is taking one responsibility. This is only meant for report generation. Now, the client asks you to give a facility to generate the report in PDF and this class will do that.
- public class PDFReportGenerationHelper : ReportGenerationHelper
- {
- public override string GenerateReport(Customer customer)
- {
- string generatedPath = null;
-
-
-
-
-
-
- generatedPath = @"%appdata%\Customer\Report.pdf";
-
- return generatedPath;
- }
- }
L = Liskov substitution principle (LSP)
This principle says “You should be able to use any derived class instead of a parent class and have it behave in the same manner without modification". In other terms, "Derived types must be completely substitutable for their base types and no new exception can be thrown by the subtype”
In the below image, I have a
basic Nokia Phone at left side; which will allow to mak a Call, Text a
message, and other features like Bluetooth, KeyPad, Front camera and Rear
camera. And I have another Nokia phone at right side in Smart version that is
smartphone, this will do the same as in basic phone like make a Call, Text
a Message, and other feature as Bluetooth, Front camera and Rear camera.
However, basic phone has non-touch screen and smartphone has touch screen, here
display screen of smartphone is completely substituted from the basic phone.
Let’s see with the Customer class as an example.
- Titanium customer has access to the CLUB only.
- Gold customer has access to both, CLUB and RESORT
- Platinum customer has access to both, CLUB and RESORT
Now, let’s see how our class looks.
- public abstract class CustomerRelationship
- {
- public abstract List<string> GetClubAccessDetails();
-
- public abstract List<string> GetResortAccessDetails();
- }
-
- public class GoldCustomer : CustomerRelationship
- {
- public override List<string> GetClubAccessDetails()
- {
-
- return new List<string>() { "CArea1", "CArea2", "CArea3", "CArea4", "CArea5" };
- }
-
- public override List<string> GetResortAccessDetails()
- {
-
- return new List<string>() { "RArea1", "RArea2", "RArea3", "RArea4", "RArea5" };
- }
- }
-
-
- public class PlatinumCustomer : CustomerRelationship
- {
- public override List<string> GetClubAccessDetails()
- {
-
- return new List<string>() { "CArea1", "CArea2", "CArea3", "CArea4", "CArea5" };
- }
-
- public override List<string> GetResortAccessDetails()
- {
-
- return new List<string>() { "RArea1", "RArea2", "RArea3", "RArea4", "RArea5" };
- }
- }
-
- public class TitaniumCustomer : CustomerRelationship
- {
- public override List<string> GetClubAccessDetails()
- {
-
- return new List<string>() { "CArea1", "CArea2", "CArea3", "CArea4", "CArea5" };
- }
-
- public override List<string> GetResortAccessDetails()
- {
- throw new NotImplementedException();
- }
- }
You may have noticed that TitaniumCustomer class has only implementation for Access Club and NO Access of Resort.
When I want to get the customer details, here is the code.
- public void GetResortAccessCustomer(List<CustomerRelationship> customerRelationships)
- {
- foreach (var cust in customerRelationships)
- {
- cust.GetResortAccessDetails();
- }
- }
Here, when I want to check if the customer has the Resort Access, this program throws the exceptions during runtime when the Customer is a TitaniumCustomer.
Now, how can we resolve the dependence?
So, the LISKOV principle says “No new exception can be thrown by the subtype, the parent should easily replace the child object”. So to adhere with LISKOV principle, we need to create two interfaces one is for Club and other for Resort, as shown below.
- public interface IClub
- {
- List<string> GetClubAccessDetails();
- }
-
- public interface IResort
- {
- List<string> GetResortAccessDetails();
- }
Now, TitaniumCustomer class will have only Club access by inheriting the IClub.
PlatinumCustomer and GoldCustomer class will remain the same with CustomerRelationship.
- public abstract class CustomerRelationship : IClub, IResort
- {
- public abstract List<string> GetClubAccessDetails();
-
- public abstract List<string> GetResortAccessDetails();
- }
-
- public class GoldCustomer : CustomerRelationship
- {
- public override List<string> GetClubAccessDetails()
- {
-
- return new List<string>() { "CArea1", "CArea2", "CArea3", "CArea4", "CArea5" };
- }
-
- public override List<string> GetResortAccessDetails()
- {
-
- return new List<string>() { "RArea1", "RArea2", "RArea3", "RArea4", "RArea5" };
- }
- }
-
- public class PlatinumCustomer : CustomerRelationship
- {
- public override List<string> GetClubAccessDetails()
- {
-
- return new List<string>() { "CArea1", "CArea2", "CArea3", "CArea4", "CArea5" };
- }
-
- public override List<string> GetResortAccessDetails()
- {
-
- return new List<string>() { "RArea1", "RArea2", "RArea3", "RArea4", "RArea5" };
- }
- }
-
-
- public class TitaniumCustomer : IClub
- {
- public List<string> GetClubAccessDetails()
- {
-
- return new List<string>() { "CArea1", "CArea2", "CArea3", "CArea4", "CArea5" };
- }
- }
When I want to get the customer details, here is the code for that. This code will be type-based and will not allow the customer whose customer type is not an IResort. Now, the program will not throw an exception during the runtime and it can easily be identified in compile time.
- public void GetResortAccessCustomer(List<CustomerRelationship> customerRelationships)
- {
- foreach (var cust in customerRelationships)
- {
- cust.GetResortAccessDetails();
- }
- }
I = Interface segregation principle (ISP)
This principle says “Any client should not be forced to use an interface which is irrelevant to it”.
In another way, one fat interface needs to split into several smaller and relevant interfaces so that the clients can know about the interfaces that are relevant for them.
Now, let’s consider. When the client uses the application with a customer login ID, the tool logs the data in Mongo DB as an Audit trail message (like Customer ID, DateTime, the operation performed and etc.)
Here, we have an Interface IDatabaseLogManager which will have an Add() signature and it is inherited to MongoDBLogger.
- public interface IDatabaseLogManager
- {
- void Add(string message, string LogType);
- }
- public class MongoDBLogger : IDatabaseLogManager
- {
- public void Add(string message, string LogType)
- {
-
- }
- }
Now my application logs the audit trail in Mongo DB.
Later on, some new clients come up with a demand saying that we also want a method which will help us to “Read” Audit trail that is logged in MongoDB. Now developers who are highly enthusiastic would like to change the “IDatabaseLogManager” interface as shown below.
- public interface IDatabaseLogManager
- {
- void Add(string message, string LogType);
- List<string> Get(string LogType);
- }
But by doing so we have done something terrible, can you guess what?
If you visualize the new requirement which has come up, you have two kinds of clients:
- Those who want to use only the “Add” method.
- The others who want to use “Add” + “Get”.
Now by changing the current interface will disturbing all the existing/old clients, even when they are not interested in the “Get” method. You are forcing them to use the “Get” method and violating ISP. ISP says “Any client should not be forced to use an interface which is irrelevant to it.”
So a better approach would be to keep existing clients in their own sweet world and the serve the new clients separately.
So, a better solution would be to create a new interface rather than updating the current interface. So we can keep the current interface “IDatabaseLogManager” as it is and add a new interface “IDatabaseLogManagerV2” with the “Get” method the “V2” stands for version 2.
- public interface IDatabaseLogManagerV2
- {
- List<string> Get(string LogType);
- }
- public class MongoDBLoggerV2 : IDatabaseLogManagerV2, IDatabaseLogManager
- {
- public List<string> Get(string LogType)
- {
-
- return new List<string>();
- }
-
- public void Add(string message, string LogType)
- {
-
- }
- }
So the existing/old clients will continue using the “IDatabaseLogManager” interface while new client can use “IDatabaseLogManagerV2” interface.
D = Dependency inversion principle (DIP)
This principle says “The high-level modules/classes should not depend on low-level modules/classes. Both should depend upon abstractions. Secondly, abstractions should not depend upon details. Details should depend upon abstractions.”
We should not write any tightly coupled code because when the application is growing bigger and bigger, if a class depends on another class, then we need to change one class if something changes in that dependent class. We should always try to write loosely coupled class.
Let's consider the Notification message to the customer. The notification will be sent by Email.
- public class Email
- {
- public void SendMessage()
- {
-
- }
- }
- public class Notification
- {
- private Email _email = null;
- public Notification()
- {
- _email = new Email();
- }
- public void Notify()
- {
- _email.SendMessage();
- }
- }
Now Notification class totally depends on the Email class, because it only sends one type of notification. If we want to introduce any other notification services like SMS, what happens then? We need to change the notification system also. And this will lead to tight coupling.
What can we do to make it loosely coupled? Here is the following implementation as below. Define the interface as IMessaging which will have a signature of SendMessage(). Implement the SendMessage() in Email, SMS and Whatsapp class.
- public interface IMessaging
- {
- void SendMessage();
- }
-
- public class Email : IMessaging
- {
- public void SendMessage()
- {
-
- }
- }
- public class SMS : IMessaging
- {
- public void SendMessage()
- {
-
- }
- }
- public class Whatsapp : IMessaging
- {
- public void SendMessage()
- {
-
- }
- }
- public class Notification
- {
- private IMessaging _messaging;
- public Notification(IMessaging messaging)
- {
- _messaging = messaging;
- }
-
- public void Notify()
- {
- _messaging.SendMessage();
- }
- }
Now Notification class used as constructor injection which will make it loosely coupled and satisfies any type of notification services. This kind of design principle will have a complete abstraction on high level and low-level modules.
A quick glance at SOLID principles
S = Single responsibility principle (SRP)
Every software module/class should have only one reason to change.
O = Open closed principle (OCP)
A software module/class is open for extension and closed for modification.
L = Liskov substitution principle (LSP)
You should be able to use any derived class instead of a parent class and have it behave in the same manner without modification. In other way, derived types must be completely substitutable for their base types and no new exception can be thrown by the subtype.
I = Interface segregation principle (ISP)
Any client should not be forced to use an interface which is irrelevant to it.
D = Dependency inversion principle (DIP)
The high-level modules/classes should not depend on low-level modules/classes. Both should depend upon abstractions.
I hope this article would help you lot on understanding on the design principles and I have shared the same source code in
GitHub(SOLID PRINCIPLE). Thanks for reading my article and I am looking for your kind feedback.