Deep Dive Into SOLID Principles

In this article, you will learn about SOLID principles with real time examples.

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.
 
SOLID Principles  SOLID Principles  SOLID Principles  SOLID Principles
 
Let’s see with a C# example.
 
Consider we have Customer and ReportGeneration classes for the application.
  1. public class CustomerHelper  
  2. {  
  3.     public void AddCustomer(Customer customer)  
  4.     {  
  5.         //Add customer into records/DB  
  6.     }  
  7.   
  8.     public void UpdateCustomer(Customer customer)  
  9.     {  
  10.         //Update customer into records/DB  
  11.     }  
  12.   
  13.     public void DeleteCustomer(int id)  
  14.     {  
  15.         //Delete customer into records/DB  
  16.     }  
  17.   
  18.     public Customer GetCustomer(int id)  
  19.     {  
  20.         //Reterive customer into records/DB  
  21.         return new Customer();  
  22.     }  
  23.   
  24.     public string GenerateReport(Customer customer)  
  25.     {  
  26.         //Generates the report of Excel, Word or PDF  
  27.         return @"%appdata%\Customer\Report.pdf";  
  28.     }  
  29.  }  
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.
  1.   public class CustomerHelper  
  2.   {  
  3.        public void AddCustomer(Customer customer)  
  4.        {  
  5.            //Add customer into records/DB  
  6.        }  
  7.        . . .  
  8.        . . .  
  9.        . . .  
  10.        public Customer GetCustomer(int id)  
  11.        {  
  12.            //Retrieve customer into records/DB  
  13.            return new Customer();  
  14.        }  
  15.   }  
  16.   
  17.   
  18.   public class ReportGenerationHelper  
  19.   {  
  20.        public string GenerateReport(Customer customer)  
  21.        {  
  22.            //Generates the report of Excel, Word or PDF  
  23.            return @"%appdata%\Customer\Report.pdf";  
  24.        }  
  25.    }  

O = Open closed principle (OCP)

 
This principle says “A software module/class is open for extension and closed for modification”.
 
SOLID Principles 
 
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.
  1. public class ReportGenerationHelper  
  2.   {  
  3.       public string GenerateReport(Customer customer)  
  4.       {  
  5.           //Generates the report of Excel, Word or PDF  
  6.           return @"%appdata%\Customer\Report.pdf";  
  7.       }  
  8.   }  
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.
  1. public class ReportGenerationHelper  
  2.  {  
  3.      public string ReportType { getset; }  
  4.   
  5.      public string GenerateReport(Customer customer)  
  6.      {  
  7.          string generatedPath = null;  
  8.          if (ReportType == "EXCEL")  
  9.          {  
  10.              // Generation logic for Excel report file  
  11.              // ....  
  12.              generatedPath = @"%appdata%\Customer\Report.xls";  
  13.          }  
  14.          else if (ReportType == "PDF")  
  15.          {  
  16.              // Generation logic for PDF report file  
  17.              // ....  
  18.              generatedPath = @"%appdata%\Customer\Report.pdf";  
  19.          }  
  20.          else if (ReportType == "WORD")  
  21.          {  
  22.              // Generation logic for Word report file  
  23.              // ....  
  24.              generatedPath = @"%appdata%\Customer\Report.docx";  
  25.          }  
  26.          return generatedPath;  
  27.      }  
  28.  }  
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.
  1. public class ReportGenerationHelper  
  2.  {  
  3.      public string ReportType { getset; }  
  4.   
  5.      public virtual string GenerateReport(Customer customer)  
  6.      {  
  7.          string generatedPath = null;  
  8.   
  9.          // Generation logic for Excel report file  
  10.          // ....  
  11.          // ....  
  12.          generatedPath = @"%appdata%\Customer\Report.xls";  
  13.   
  14.          return generatedPath;  
  15.      }  
  16.  }  
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.
  1. public class WordReportGenerationHelper : ReportGenerationHelper  
  2.  {  
  3.      public override string GenerateReport(Customer customer)  
  4.      {  
  5.          string generatedPath = null;  
  6.   
  7.          // Generation logic for Word report file  
  8.          // ....  
  9.          // ....  
  10.          generatedPath = @"%appdata%\Customer\Report.docx";  
  11.   
  12.          return generatedPath;  
  13.      }  
  14.  }  
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.
  1. public class PDFReportGenerationHelper : ReportGenerationHelper  
  2.  {  
  3.      public override string GenerateReport(Customer customer)  
  4.      {  
  5.          string generatedPath = null;  
  6.   
  7.          // Generation logic for PDF report file  
  8.          // ....  
  9.          // ....  
  10.          // ....  
  11.   
  12.          generatedPath = @"%appdata%\Customer\Report.pdf";  
  13.   
  14.          return generatedPath;  
  15.      }  
  16.  }  

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.
 
SOLID Principles
  • 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.
  1. public abstract class CustomerRelationship  
  2. {  
  3.     public abstract List<string> GetClubAccessDetails();  
  4.   
  5.     public abstract List<string> GetResortAccessDetails();  
  6. }  
  7.   
  8. public class GoldCustomer : CustomerRelationship  
  9. {  
  10.     public override List<string> GetClubAccessDetails()  
  11.     {  
  12.         //Access Club Area  
  13.         return new List<string>() { "CArea1""CArea2""CArea3""CArea4""CArea5" }; 
  14.     }
  15.  
  16.     public override List<string> GetResortAccessDetails()  
  17.     {  
  18.         //Access Resort Area  
  19.         return new List<string>() { "RArea1""RArea2""RArea3""RArea4""RArea5" };  
  20.     }  
  21. }  
  22.   
  23.   
  24. public class PlatinumCustomer : CustomerRelationship  
  25. {  
  26.     public override List<string> GetClubAccessDetails()  
  27.     {  
  28.         //Access Club Area  
  29.         return new List<string>() { "CArea1""CArea2""CArea3""CArea4""CArea5" };  
  30.     }  
  31.   
  32.     public override List<string> GetResortAccessDetails()  
  33.     {  
  34.         //Access Resort Area  
  35.         return new List<string>() { "RArea1""RArea2""RArea3""RArea4""RArea5" };  
  36.     }  
  37. }  
  38.   
  39. public class TitaniumCustomer : CustomerRelationship  
  40. {  
  41.     public override List<string> GetClubAccessDetails()  
  42.     {  
  43.         //Access Club Area  
  44.         return new List<string>() { "CArea1""CArea2""CArea3""CArea4""CArea5" };  
  45.     }  
  46.   
  47.     public override List<string> GetResortAccessDetails()  
  48.     {  
  49.         throw new NotImplementedException();  
  50.     }  
  51. }  
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.
 
SOLID Principles 
  1. public void GetResortAccessCustomer(List<CustomerRelationship> customerRelationships)  
  2. {  
  3.     foreach (var cust in customerRelationships)  
  4.     {  
  5.         cust.GetResortAccessDetails();  
  6.     }  
  7. }  
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.
 
SOLID Principles 
 
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. 
  1. public interface IClub  
  2. {  
  3.     List<string> GetClubAccessDetails();  
  4. }  
  5.   
  6. public interface IResort  
  7. {  
  8.     List<string> GetResortAccessDetails();  
  9. }   
Now, TitaniumCustomer class will have only Club access by inheriting the IClub.
 
PlatinumCustomer and GoldCustomer class will remain the same with CustomerRelationship.
  1. public abstract class CustomerRelationship : IClub, IResort  
  2. {  
  3.     public abstract List<string> GetClubAccessDetails();  
  4.   
  5.     public abstract List<string> GetResortAccessDetails();  
  6. }  
  7.   
  8. public class GoldCustomer : CustomerRelationship  
  9. {  
  10.     public override List<string> GetClubAccessDetails()  
  11.     {  
  12.         //Access Club Area  
  13.         return new List<string>() { "CArea1""CArea2""CArea3""CArea4""CArea5" };  
  14.     }  
  15.   
  16.     public override List<string> GetResortAccessDetails()  
  17.     {  
  18.         //Access Resort Area  
  19.         return new List<string>() { "RArea1""RArea2""RArea3""RArea4""RArea5" };  
  20.     }  
  21. }  
  22.   
  23. public class PlatinumCustomer : CustomerRelationship  
  24. {  
  25.     public override List<string> GetClubAccessDetails()  
  26.     {  
  27.         //Access Club Area  
  28.         return new List<string>() { "CArea1""CArea2""CArea3""CArea4""CArea5" };  
  29.     }  
  30.   
  31.     public override List<string> GetResortAccessDetails()  
  32.     {  
  33.         //Access Resort Area  
  34.         return new List<string>() { "RArea1""RArea2""RArea3""RArea4""RArea5" };  
  35.     }  
  36. }   
  37.   
  38.   
  39. public class TitaniumCustomer : IClub  
  40. {  
  41.     public List<string> GetClubAccessDetails()  
  42.     {  
  43.         //Access Club Area  
  44.         return new List<string>() { "CArea1""CArea2""CArea3""CArea4""CArea5" };  
  45.     }  
  46. }  
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.
 
SOLID Principles 
  1. public void GetResortAccessCustomer(List<CustomerRelationship> customerRelationships)  
  2.        {  
  3.            foreach (var cust in customerRelationships)  
  4.            {  
  5.                cust.GetResortAccessDetails();  
  6.            }  
  7.  

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.
 
SOLID Principles 
 
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.
  1. public interface IDatabaseLogManager  
  2.     {  
  3.         void Add(string message, string LogType);  
  4.     }   
  5. public class MongoDBLogger : IDatabaseLogManager  
  6.     {  
  7.         public void Add(string message, string LogType)  
  8.         {  
  9.             // Write in DB  
  10.         }  
  11.     }  
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. 
  1. public interface IDatabaseLogManager  
  2. {  
  3.     void Add(string message, string LogType); // For Old/Existing customer  
  4.     List<string> Get(string LogType);// For New customer  
  5. }   
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. 
  1. public interface IDatabaseLogManagerV2  
  2.  {  
  3.         List<string> Get(string LogType);  
  4.  }  
  5.  public class MongoDBLoggerV2 : IDatabaseLogManagerV2, IDatabaseLogManager  
  6.  {  
  7.         public List<string> Get(string LogType)  
  8.         {  
  9.             // Read log from DB  
  10.             return new List<string>();  
  11.         }  
  12.   
  13.         public void Add(string message, string LogType)  
  14.         {  
  15.             // Write in DB  
  16.         }  
  17.   }  
So the existing/old clients will continue using the “IDatabaseLogManager” interface while new client can use “IDatabaseLogManagerV2” interface.
 
SOLID Principles 
 

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.”
 
SOLID Principles 
 
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.
  1. public class Email  
  2. {  
  3.         public void SendMessage()  
  4.         {  
  5.   
  6.         }  
  7.  }  
  8.  public class Notification  
  9.  {  
  10.         private Email _email = null;  
  11.         public Notification()  
  12.         {  
  13.             _email = new Email();  
  14.         }  
  15.         public void Notify()  
  16.         {  
  17.             _email.SendMessage();  
  18.         }  
  19.  }  
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.
  1. public interface IMessaging  
  2. {  
  3.         void SendMessage();  
  4. }  
  5.   
  6. public class Email : IMessaging  
  7. {  
  8.         public void SendMessage()  
  9.         {  
  10.             // Message sent by Email  
  11.         }  
  12. }  
  13. public class SMS : IMessaging  
  14. {  
  15.         public void SendMessage()  
  16.         {  
  17.             // Message sent by SMS  
  18.         }  
  19. }  
  20. public class Whatsapp : IMessaging  
  21. {  
  22.         public void SendMessage()  
  23.         {  
  24.             // Message sent by Whatsapp  
  25.         }  
  26. }  
  27. public class Notification  
  28. {  
  29.         private IMessaging _messaging;  
  30.         public Notification(IMessaging messaging)  
  31.         {  
  32.             _messaging = messaging;  
  33.         }  
  34.   
  35.         public void Notify()  
  36.         {  
  37.             _messaging.SendMessage();  
  38.         }  
  39. }  
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.