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.
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.
- public class Customer
- {
- void Add()
- {
- try
- {
-
- }
- catch (Exception ex)
- {
- File.WriteAllText(@"C:\Error.txt", ex.ToString());
- }
- }
- }
Solution
Abstracting functionality of error logging so we no longer violate the single responisbility principle:
- public class Customer
- {
- void Add()
- {
- try
- {
- // TODO: Calling Add method DAL
- }
- catch (Exception ex)
- {
- FileLogger logger = new FileLogger();
- logger.Handle(ex.Message);
- }
- }
- }
-
- public class FileLogger
- {
- public void Handle(string error)
- {
- File.WriteAllText(@"C:\Error.txt", error);
- }
- }
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
- public class Checkout
- {
- public string Merchant { get; set; }
-
- public double CalculateShippingCost(double orderAmount)
- {
- double shippingCost = 0;
- switch (Merchant)
- {
- case "Flipkart":
- shippingCost = orderAmount + (orderAmount * 0.10);
- break;
- case "Amazon":
- shippingCost = orderAmount + (orderAmount * 0.05);
- break;
- default:
- break;
- }
- return shippingCost;
- }
- }
Solution
By creating inheritance, we make it easy to modify by adding a derived class without touching the existing class
- public class Checkout
- {
- public virtual double CalculateShippingCost(double orderAmount)
- {
- return orderAmount;
- }
- }
-
- class Flipkart : Checkout
- {
- public override double CalculateShippingCost(double orderAmount)
- {
- return orderAmount + (orderAmount * 0.10);
- }
- }
-
- class Amazon : Checkout
- {
- public override double CalculateShippingCost(double orderAmount)
- {
- return orderAmount + (orderAmount * 0.05);
- }
- }
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.
- abstract class Payment
- {
- public abstract void CheckBalance();
-
- public abstract void DeductAmount();
-
- public abstract void ProcessTransaction();
-
- }
-
- class Paypal : Payment
- {
- public override void CheckBalance()
- {
- Console.WriteLine("CheckBalance Method Called");
- }
-
- public override void DeductAmount()
- {
- Console.WriteLine("DeductAmount Method Called");
- }
-
- public override void ProcessTransaction()
- {
- Console.WriteLine("ProcessTransaction Method Called");
- }
- }
-
- class COD : Payment
- {
- public override void CheckBalance()
- {
- throw new NotImplementedException();
- }
-
- public override void DeductAmount()
- {
- throw new NotImplementedException();
- }
-
- public override void ProcessTransaction()
- {
- Console.WriteLine("ProcessTransaction Method Called");
- }
- }
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.
- interface IPaymentTransaction
- {
- void ProcessTransaction();
- }
-
- interface IPaymentCheckBalance
- {
- void CheckBalance();
- void DeductAmount();
- }
-
- class Paypal : IPaymentTransaction, IPaymentCheckBalance
- {
- public void CheckBalance()
- {
- Console.WriteLine("CheckBalance Method Called");
- }
-
- public void DeductAmount()
- {
- Console.WriteLine("DeductAmount Method Called");
- }
-
- public void ProcessTransaction()
- {
- Console.WriteLine("ProcessTransaction Method Called");
- }
- }
-
- class COD : IPaymentTransaction
- {
- public void ProcessTransaction()
- {
- Console.WriteLine("ProcessTransaction Method Called");
- }
- }
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.
- public interface ICustomer
- {
- void Add();
- void Read();
- }
Solution
By creating another interface and extending from it, we can avoid violating the above scenario.
- public interface ICustomer
- {
- void Add();
- }
-
- public interface ICustomerRead : ICustomer
- {
- void Read();
- }
-
- class Client : ICustomer, ICustomerRead
- {
- public void Add()
- {
- Console.WriteLine("Add functionality");
- }
-
- public void Read()
- {
- Console.WriteLine("Read functionality");
- }
- }
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.
- public class Customer
- {
- private IErrorHandling _errorHandling = new FileLogger();
-
- public void Add()
- {
- try
- {
-
- }
- catch (Exception ex)
- {
- _errorHandling.Handle(ex.Message);
- }
- }
- }
-
- public interface IErrorHandling
- {
- void Handle(string error);
- }
- public class FileLogger : IErrorHandling
- {
- public void Handle(string error)
- {
- Console.WriteLine("file log");
- }
- }
Solution
By injecting any dependencies of a class through the class constructor as an input parameter.
- public class Customer
- {
- private IErrorHandling _errorHandling;
- public Customer(IErrorHandling errorHandling)
- {
- _errorHandling = errorHandling;
- }
- public void Add()
- {
- try
- {
- // TODO: Calling Add method DAL
- }
- catch (Exception ex)
- {
- _errorHandling.Handle(ex.Message);
- }
- }
- }
-
- public interface IErrorHandling
- {
- void Handle(string error);
- }
- public class FileLogger : IErrorHandling
- {
- public void Handle(string error)
- {
- Console.WriteLine("file log");
- }
- }
-
- public class EmailLogger : IErrorHandling
- {
- public void Handle(string error)
- {
- Console.WriteLine("inserting to db");
- }
- }
Summary
In this article, we have learned simple examples of the SOLID principles.