S.O.L.I.D. design principles

S.O.L.I.D. are five object oriented design principles which if implemented properly can help us in creating software solutions which are easier to maintain and extend. These principles were given by Robert C. Martin or Uncle Bob as commonly known.S.O.L.I.D. Stands for:

solid design principles

Let’s understand each one of them in detail :

  • Single Responsibility Principle : This is the simplest of all five principles. It states that  a class should be responsible for only one functionality and it should not cater to multiple needs. Let’s see this in action by following example: Here we have 2 classes one for Retail Customer and other for WholeSale Customer. Now we need a class that calculates discount price for each type of customer lets say that class is CalculateDiscount.
enum CustomerType
{
 RetailCustomer,
 WholeSaleCustomer
}

class RetailCustomer
{
 private int _totalItems;
 private float _totalAmount;

 public RetailCustomer(int totalItems, float totalAmount)
 {
 this._totalItems = totalItems;
 this._totalAmount = totalAmount;
 }
 }

 class WholeSaleCustomer
{
 private int _totalItems;
 private float _totalAmount;

 public WholeSaleCustomer(int totalItems, float totalAmount)
 {
 this._totalItems = totalItems;
 this._totalAmount = totalAmount;
 }
 }

 class CalculateDiscount
{
 private CustomerType _typeOfCustomer;

 public CalculateDiscount(CustomerType typeOfCustomer)
 {
 _typeOfCustomer = typeOfCustomer;
 }

 public float CalculateDiscountPrice()
 {
 float discount = 0;

 //Logic goes here

 return discount;
 }

 public string GetDiscount()
 {
 return (string.Format("Calculated discount is {0}", CalculateDiscountPrice()));
 }
 }

Here if you see CalculateDiscount class does 2 things (violates single responsibility principle), Calculates the discount and show the output as well through another method GetDiscount. Now, lets see if there is customer who wants to see the output in different way then this class again needs to be modified for that customer by adding an if else block.

To fix this, what we can do is remove the GetDiscount() from CalculateDiscount class & create another class that is whole solely dedicated to showing just the outputs . Let’s name this class as CalculateDiscountOutputter. Now we can have different methods in this class let’s say with names like:  GetDiscountOutputHTMLFormat, GetDiscountOutputXMLFormat, GetDiscountOutputJSONFormat etc. Now we have the code which totally complies with Single repsonsibility principle.

  • Open Closed Principle:  This states that a class should be easily extendable without modifying the class itself. Let’s take a look at our CalculateDiscount class and its CalculateDiscountPrice method.
    internal class CalculateDiscount
     {
     private CustomerType _typeOfCustomer;
    
     public CalculateDiscount(CustomerType typeOfCustomer)
     {
     _typeOfCustomer = typeOfCustomer;
     }
    
     public float CalculateDiscountPrice()
     {
     float discount = 0;
     int discountPercentage = 0;
    
     //Logic goes here
     switch (_typeOfCustomer)
     {
     case CustomerType.RetailCustomer:
     discountPercentage = 3;
     break;
     case CustomerType.WholeSaleCustomer:
     discountPercentage = 7;
     break;
     }
    
     //Calculate discount on basis of discount % and other factors
    
     return discount;
     }
    
     }

    Now if another type of customer is added then this method needs to be modified to add one more case statement for that customer. This is against the open closed principle. Hence to fix this what we can do is move the code to decide the discount percentage in each specific customer class. That ways whenever a new customer is added other classes doesn’t need to be changed, only a new class need to be created specifically for that customer.

    This also makes sure that we do not end up retesting whole code again and again for every new customer addition.

  • Liskov Substitution Principle: This states that, parent class object should easily refer a child class object without disturbing or changing base class behaviour. Take same example as we have been using throughout. I have modified it a bit. Created a common base class called as Customer and inherited old classes RetailCustomer & WholeSaleCustomer from base class Customer.
internal class Customer
 {
 public virtual int TotalItems { get; set; }
 public virtual float TotalAmount { get; set; }
 public virtual int DiscountPercentage { get; set; }

 public virtual float CalculateDiscountPrice(float totalAmount)
 {
 float discount = 0;
 //Logic goes here to manipulate discount

 return discount;
 }

 public virtual void Add()
 {
 //add customer to Database
 }
 }

 internal class RetailCustomer : Customer
 {
 public override float CalculateDiscountPrice(float totalAmount)
 {
 return (base.CalculateDiscountPrice(totalAmount) - 10);
 }

 }

 internal class WholeSaleCustomer : Customer
 {
 public override float CalculateDiscountPrice(float totalAmount)
 {
 return (base.CalculateDiscountPrice(totalAmount) - 50);
 }

 }

Let’s say now we need another type of customer which is dummy customer and not the actual one. This customer should have all existing functionality except the fact that it cannot be added in database hence, we are overriding behavior of base class Add method in this Dummy Customer class.

internal class DummyCustomer : Customer
 {
 public override float CalculateDiscountPrice(float totalAmount)
 {
 return (base.CalculateDiscountPrice(totalAmount));
 }
 public override void Add()
 {
 throw new Exception("Add not allowed for Dummy Customer");
 }
 }

Now consider below code :

private static void Main(string[] args)
 {
 //Retail Customer
 Customer customer = new RetailCustomer {TotalAmount = 50, TotalItems = 5, DiscountPercentage = 3};

 customer .Add();
 customer .CalculateDiscountPrice(customer.TotalAmount);

 //WholeSale Customer
 customer = new WholeSaleCustomer {TotalAmount = 100, TotalItems = 10, DiscountPercentage = 7};

 customer .Add();
 customer .CalculateDiscountPrice(customer.TotalAmount);

 //Dummy Customer
 customer = new DummyCustomer {TotalAmount = 70, TotalItems = 7, DiscountPercentage = 0};
 customer .CalculateDiscountPrice(customer.TotalAmount);

 customer.Add();
}

This code will result in an exception as soon as “ dummy.Add();” is executed since in overridden method an exception is thrown for dummy customer. Now to fix this what we can do is we can divide the functionality of concrete class customer into interfaces in following way:

internal interface IDiscountFunctions
 {
 float CalculateDiscountPrice(float totalAmount);
 }

 internal interface IDatabaseFunctions
 {
 void Add();
 }

Hence now in dummy customer we can inherit from customer class which in turn implements IDiscountFunctions. Add function is removed from Customer class, instead IDatabaseFunction interface is implemented for Retail & Wholesale customers.

internal class Customer : IDiscountFunctions
 {
 public virtual int TotalItems { get; set; }
 public virtual float TotalAmount { get; set; }
 public virtual int DiscountPercentage { get; set; }

 public virtual float CalculateDiscountPrice(float totalAmount)
 {
 float discount = 0;
 //Logic goes here to manipulate discount

 return discount;
 }
 }

 internal class RetailCustomer : Customer,IDatabaseFunctions
 {
 public override float CalculateDiscountPrice(float totalAmount)
 {
 return (base.CalculateDiscountPrice(totalAmount) - 10);
 }

public void Add()
 {
 //DB logic goes here
 }
 }

 internal class WholeSaleCustomer : Customer, IDatabaseFunctions
 {
 public override float CalculateDiscountPrice(float totalAmount)
 {
 return (base.CalculateDiscountPrice(totalAmount) - 50);
 }

 public void Add()
 {
 //DB logic goes here
 }
}

 internal class DummyCustomer : Customer
 {
 public override float CalculateDiscountPrice(float totalAmount)
 {
 return (base.CalculateDiscountPrice(totalAmount));
 }
 }

private static void Main(string[] args)
 {
 //Retail Customer
 Customer customer = new RetailCustomer {TotalAmount = 50, TotalItems = 5, DiscountPercentage = 3};
 IDatabaseFunctions customerFunctions = new RetailCustomer();

 customerFunctions.Add();
 customer.CalculateDiscountPrice(customer.TotalAmount);

 //WholeSale Customer
 customer = new WholeSaleCustomer {TotalAmount = 100, TotalItems = 10, DiscountPercentage = 7};
 customerFunctions = new WholeSaleCustomer();

 customerFunctions.Add();
 customer.CalculateDiscountPrice(customer.TotalAmount);

 //Dummy Customer
 customer = new DummyCustomer {TotalAmount = 70, TotalItems = 7, DiscountPercentage = 0};
 customerFunctions = new DummyCustomer();

 customer.CalculateDiscountPrice(customer.TotalAmount);
 }

Now as soon as this line is types “customerFunctions = new DummyCustomer(); ” user will be notified of compiler error that source type DummyCustomer cannot be converted to target type IDatabaseFunctions. To run this, just remove this line  “customerFunctions = new DummyCustomer(); “

Now our parent class object can easily refer any child class objects without changing behavior.

  • Interface segregation principle : This states that a client should never be forced to implement an interface it client doesn’t need it or use it.Let’s take a scenario when one of the customer type need us to provide them the functionality to update data as well in Database in addition to Add method. So the normal approach would be to add Update() in IDatabaseFunctions interface and lets classes implement them in the way they need. But there is a flaw in this, doing this will ensure that all kind of customers who implement IDatabaseFunctions interface need to implement Update() even if they do not need it. Hence to avoid this we can have another interface which will have this update function, without changing the previous interface. In this way we can provide update functionality specifically to those customers who need it without forcing all customers to implement update functionality.

  • Dependency Inversion Principle : This basically states that higher level modules and lower level modules should be loosely coupled. Higher level module should depend on abstractions of lower level modules and not on concrete classes.Let’s take an example of Add function we were using. For adding anything to Database obviously we need Database connection. Hence conveniently we can use below code :
public void Add(SqlDbConnection connection)
 {
 //DB logic goes here.
 }

But the problem occurs when there are some client do not want to use sql db connection instead they want to use oracle db connection. So now if you go back and modify for each customer you are violating Open close principle , as here Add and DB connection are tightly coupled.

To fix this we can create an interface for DB Connection which can have a connect method and other command methods. This interface can be implemented by various classes like Sql db connection class or oracle DB connection class. And in Add() we can pass a reference to this interface depending on user’s requirement. Let’s see this in action :

internal interface IDatabaseFunctions
 {
 void Add(IDbConnection connection);
 }

 internal interface IDbConnection
 {
 void Connect();
 }

 internal class SqlDbConnection : IDbConnection
 {
 public void Connect()
 {
 //code related to Sql server connection and commands
 }
 }

 internal class OracleDbConnection :IDbConnection
 {
 public void Connect()
 {
 //code related to Oracle connection and commands
 }
 }

private static void Main(string[] args)
 {
 //Retail Customer
 Customer customer = new RetailCustomer {TotalAmount = 50, TotalItems = 5, DiscountPercentage = 3};
 IDatabaseFunctions customerFunctions = new RetailCustomer();
 IDbConnection dbConnection = new SqlDbConnection();
 customerFunctions.Add(dbConnection);
 customer.CalculateDiscountPrice(customer.TotalAmount);

 //WholeSale Customer
 customer = new WholeSaleCustomer {TotalAmount = 100, TotalItems = 10, DiscountPercentage = 7};
 customerFunctions = new WholeSaleCustomer();
 dbConnection=new OracleDbConnection();

 customerFunctions.Add(dbConnection);
 customer.CalculateDiscountPrice(customer.TotalAmount);
 }

This solves another problem for us. If in future another client comes up who wants to use another DB, take for example MY SQL, then we just need to add one more class for MYSQL and implement this interface and we are pretty much done.

SOLID Principles surely helps up in creating code which can be effectively maintained and extended hence removing a lot of burden from our shoulders if implemented properly.

Advertisements

3 thoughts on “S.O.L.I.D. design principles

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s