SOLID With a Simple Example

Introduction

 
In this article, we will learn the SOLID principle with a simple example. In an interview, this is a common question for candidates with 2+ years of experience. Also for creating any simple software design, this is very important to consider in our project. 
 
Download the source code from this GitHub URL.
 
SOLID are five basic principles that help to create good software architecture. SOLID is an acronym.
  • S - SRP (Single responsibility principle)
  • O - OCP (Open closed principle)
  • L - LSP (Liskov substitution principle)
  • I - ISP (Interface segregation principle)
  • D - DIP (Dependency inversion principle)

Single Responsibility Principle (SRP)

 
SRP says that classes or modules should have only one responsibility and not multiple.
 
Problem
 
We can notice that in below code, the Add method has too many responsibilities, where it will have an add method to DB and error handling logic. This violates the single responsibility principle.
  1. public class Customer  
  2. {  
  3.     void Add()  
  4.     {  
  5.         try  
  6.         {  
  7.             // TODO: Calling Add method DAL 
  8.         }  
  9.         catch (Exception ex)  
  10.         {  
  11.             File.WriteAllText(@"C:\Error.txt", ex.ToString());  
  12.         }  
  13.     }  
  14. }  
Solution
 
Abstracting functionality of error logging so we no longer violate the single responisbility principle:
  1. public class Customer  
  2. {  
  3.     void Add()  
  4.     {  
  5.         try  
  6.         {  
  7.             // TODO: Calling Add method DAL
  8.         }  
  9.         catch (Exception ex)  
  10.         {  
  11.             FileLogger logger = new FileLogger();  
  12.             logger.Handle(ex.Message);  
  13.         }  
  14.     }  
  15. }  
  16.   
  17. public class FileLogger  
  18. {  
  19.     public void Handle(string error)  
  20.     {  
  21.         File.WriteAllText(@"C:\Error.txt", error);  
  22.     }  
  23. }  

Open Closed Principle (OCP)

 
OCP says that software entities (classed, modules, methods, etc) should be open for extensions, but closed for modification.
 
Problem
 
We can notice in below code that it is violating the Open Closed Principle, because if we need to add another company (like big basket) we need to modify the existing code in the switch statement
  1. public class Checkout  
  2. {  
  3.     public string Merchant { getset; }  
  4.   
  5.     public double CalculateShippingCost(double orderAmount)  
  6.     {  
  7.         double shippingCost = 0;  
  8.         switch (Merchant)  
  9.         {  
  10.             case "Flipkart":  
  11.                 shippingCost = orderAmount + (orderAmount * 0.10);  
  12.                 break;  
  13.             case "Amazon":  
  14.                 shippingCost = orderAmount + (orderAmount * 0.05);  
  15.                 break;  
  16.             default:  
  17.                 break;  
  18.         }  
  19.         return shippingCost;  
  20.     }  
  21. }  
Solution
 
By creating inheritance, we make it easy to modify by adding a derived class without touching the existing class
  1. public class Checkout  
  2. {  
  3.     public virtual double CalculateShippingCost(double orderAmount)  
  4.     {  
  5.         return orderAmount;  
  6.     }  
  7. }  
  8.   
  9. class Flipkart : Checkout  
  10. {  
  11.     public override double CalculateShippingCost(double orderAmount)  
  12.     {  
  13.         return orderAmount + (orderAmount * 0.10);  
  14.     }  
  15. }  
  16.   
  17. class Amazon : Checkout  
  18. {  
  19.     public override double CalculateShippingCost(double orderAmount)  
  20.     {  
  21.         return orderAmount + (orderAmount * 0.05);  
  22.     }  
  23. }  

Liskov substitution principle (LSP)

 
LSP says that a parent class should be able to refer child objects seamlessly during runtime polymorphism
 
Problem
 
We can notice in the below code, for COD (Cash on deliver) transaction there is no need to check the balanace method/feature and deduct an amount method/feature, but the client is still using it. 
  1. abstract class Payment  
  2. {  
  3.     public abstract void CheckBalance();  
  4.   
  5.     public abstract void DeductAmount();  
  6.   
  7.     public abstract void ProcessTransaction();  
  8.   
  9. }  
  10.   
  11. class Paypal : Payment  
  12. {  
  13.     public override void CheckBalance()  
  14.     {  
  15.         Console.WriteLine("CheckBalance Method Called");  
  16.     }  
  17.   
  18.     public override void DeductAmount()  
  19.     {  
  20.         Console.WriteLine("DeductAmount Method Called");  
  21.     }  
  22.   
  23.     public override void ProcessTransaction()  
  24.     {  
  25.         Console.WriteLine("ProcessTransaction Method Called");  
  26.     }  
  27. }  
  28.   
  29. class COD : Payment  
  30. {  
  31.     public override void CheckBalance()  
  32.     {  
  33.         throw new NotImplementedException();  
  34.     }  
  35.   
  36.     public override void DeductAmount()  
  37.     {  
  38.         throw new NotImplementedException();  
  39.     }  
  40.   
  41.     public override void ProcessTransaction()  
  42.     {  
  43.         Console.WriteLine("ProcessTransaction Method Called");  
  44.     }  
  45. }  
Solution
 
As a COD (Cash on deliver) transaction is only needed to do a transaction, there's no need to check the balance method/feature and deduct the amount method/feature. We can use the interface and implement it based on the scenario.
  1. interface IPaymentTransaction  
  2. {  
  3.     void ProcessTransaction();  
  4. }  
  5.   
  6. interface IPaymentCheckBalance  
  7. {  
  8.     void CheckBalance();  
  9.     void DeductAmount();  
  10. }  
  11.   
  12. class Paypal : IPaymentTransaction, IPaymentCheckBalance  
  13. {  
  14.     public void CheckBalance()  
  15.     {  
  16.         Console.WriteLine("CheckBalance Method Called");  
  17.     }  
  18.   
  19.     public void DeductAmount()  
  20.     {  
  21.         Console.WriteLine("DeductAmount Method Called");  
  22.     }  
  23.   
  24.     public   void ProcessTransaction()  
  25.     {  
  26.         Console.WriteLine("ProcessTransaction Method Called");  
  27.     }  
  28. }  
  29.   
  30. class COD : IPaymentTransaction  
  31. {  
  32.     public   void ProcessTransaction()  
  33.     {  
  34.         Console.WriteLine("ProcessTransaction Method Called");  
  35.     }  
  36. }  

Interface segragation principle (ISP)

 
ISP says show only the methods to the client which they need, i.e., no client should be forced to depend on methods they do not use.
 
Problem
 
We can notice in below code, as we add more functionalities (i.e. read method) all the client should use is read method, if it's not required.
  1. public interface ICustomer  
  2. {  
  3.     void Add();  
  4.     void Read(); // Adding read method to existing Functionality is BAD  
  5. }  
Solution
 
By creating another interface and extending from it, we can avoid violating the above scenario. 
  1. public interface ICustomer  
  2. {  
  3.     void Add();  
  4. }  
  5.   
  6. public interface ICustomerRead : ICustomer  
  7. {  
  8.     void Read();  
  9. }  
  10.   
  11. class Client : ICustomer, ICustomerRead  
  12. {  
  13.     public void Add()  
  14.     {  
  15.         Console.WriteLine("Add functionality");  
  16.     }  
  17.   
  18.     public void Read()  
  19.     {  
  20.         Console.WriteLine("Read functionality");  
  21.     }  
  22. }  

Dependency inversion principle (DIP)

 
DIP says that high-level modules should not depend on low-level modules, but rather should depend on abstraction.
 
Problem
 
We can notice that in the below code, in case if we want to change FileLogger to EmailLogger method/feature, we have to modify it in the Customer class, thus it was violating this principle.
  1. public class Customer  
  2. {  
  3.     private IErrorHandling _errorHandling = new FileLogger(); // BAD  
  4.       
  5.     public void Add()  
  6.     {  
  7.         try  
  8.         {  
  9.             // Adding logic  
  10.         }  
  11.         catch (Exception ex)  
  12.         {  
  13.             _errorHandling.Handle(ex.Message);  
  14.         }  
  15.     }  
  16. }  
  17.   
  18. public interface IErrorHandling  
  19. {  
  20.     void Handle(string error);  
  21. }  
  22. public class FileLogger : IErrorHandling  
  23. {  
  24.     public void Handle(string error)  
  25.     {  
  26.         Console.WriteLine("file log");  
  27.     }  
  28. }  
Solution
 
By injecting any dependencies of a class through the class constructor as an input parameter.
  1. public class Customer  
  2.    {  
  3.        private IErrorHandling _errorHandling;  
  4.        public Customer(IErrorHandling errorHandling)  
  5.        {  
  6.            _errorHandling = errorHandling;  
  7.        }  
  8.        public void Add()  
  9.        {  
  10.            try  
  11.            {  
  12.                // TODO: Calling Add method DAL
  13.            }  
  14.            catch (Exception ex)  
  15.            {  
  16.                _errorHandling.Handle(ex.Message);  
  17.            }  
  18.        }  
  19.    }  
  20.   
  21.    public interface IErrorHandling  
  22.    {  
  23.        void Handle(string error);  
  24.    }  
  25.    public class FileLogger : IErrorHandling  
  26.    {  
  27.        public void Handle(string error)  
  28.        {  
  29.            Console.WriteLine("file log");  
  30.        }  
  31.    }  
  32.   
  33.    public class EmailLogger : IErrorHandling  
  34.    {  
  35.        public void Handle(string error)  
  36.        {  
  37.            Console.WriteLine("inserting to db");  
  38.        }  
  39.    }  

Summary

 
In this article, we have learned simple examples of the SOLID principles.