SOLID Design Principle is a collection of best-practice,object-oriented design principles which can be applied to your application design. This allow us to accomplish various desirable goals such as loose-coupling, higher maintainability.
SOLID is an acronym where:
- (S)RP: Single Responsibility Principle
There should never be more than one reason to change any class. - (O)CP: Open Closed Principle
Software Entities (Classes, Modules, Functions, etc. ) should be open for extension but closed for any modification. - (L)SP: Liskov Substitution Principle
Function that use objects of base clasesses must be able to use objects of other dervied class wihout knowing it. - (I)SP: Interface Segregation Principle
Big interfaces should be segregated into multiple logical interfaces. - (D)IP: Dependency Inversion Principle
a. High Level Modules should not be depend upon low level modules. Both should depend upon abstractions.
b. Abstraction should not depend upon details. Details should depend upon abstraction.
Let us discuss each principle in detail.
Single Responsibility Principle
In the context of the Single Responsibility Principle (SRP) we define a responsibility to be “a reason for change.” Therefore SRP states, “An object should have only one reason to change”. If an object has more than one reason to change then it has more than one responsibility and is in violation of SRP. An object should have one and only one reason to change.
Let’s look at an example. In the example below we have a BankAccount class that has a couple of methods:
public abstract class BankAccount { double Balance { get; } void Deposit(double amount) {} void Withdraw(double amount) {} void AddInterest(double amount) {} void Transfer(double amount, IBankAccount toAccount) {} }
Let’s say that we use this BankAccount class for a person’s Checking and Savings account. That would cause this class to have more than two reasons to change. This is because Checking accounts do not have interest added to them and only Savings accounts have interest added to them on a monthly basis or however the bank calculates it.
Some people may say that the class would even have 3 reasons to change because of the Deposit/Withdraw methods as well but I think we can definitely get a little crazy with SRP. That being said, I believe it just depends on the context.
So, let’s refactor this to be more SRP friendly.
public abstract class BankAccount { double Balance { get; } void Deposit(double amount); void Withdraw(double amount); void Transfer(double amount, IBankAccount toAccount); }
public class CheckingAccount : BankAccount { }
public class SavingsAccount : BankAccount { public void AddInterest(double amount); }
So what we have done is simply create an abstract class out of BankAccount and then created a concrete CheckingAccount and SavingsAccount class so that we can isolate the methods that are causing more than one reason to change.
See another example of this principle here.
Open Closed Principle
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. According to this principle, component should be
- Open to ExtensionNew behavior can be added in the future.
- Closed to Modification
Changes to source code is not required.
Based on above statement, software entities should change behavior without changing code. For this, we have to use abstraction to implement behavior. In .NET we can use Interface and Abstract class to implement abstraction.
Let us see an example of e-commerce application to calculate cart’s total amount.
public class OrderItem { public string StockKeepingUnit { get; set; } public int Quantity { get; set; } }
public class Cart { private readonly List _items; public string CustomerEmail { get; set; } public Cart() { _items = new List(); } public IEnumerable Items { get { return _items; } } public void Add(OrderItem orderItem) { _items.Add(orderItem); } public decimal TotalAmount() { decimal total = 0; foreach (OrderItem orderItem in Items) { if (orderItem.StockKeepingUnit.StartsWith("EACH")) { total += orderItem.Quantity * 5m; } else if (orderItem.StockKeepingUnit.StartsWith("WEIGHT")) { total += orderItem.Quantity * 4m/1000; } else if (orderItem.StockKeepingUnit.StartsWith("SPECIAL")) { total += orderItem.Quantity * 4m; int setOfThree = orderItem.Quantity / 3; total -= setOfThree * 0.2m; } } return total; }
Above code is calculating amount based on quantity and StockKeepingUnit. This application is perfectly fine and will give valid result. See attached unit tests
Now we got requirement to add one more calculation condition in TotalAmount function. For this we have to change in that function code, which voilates Open Closed Principle. So
- Addiding new rule will change total calculator function everytime
- New calculation logic can introduce bug in system
- We should avoid introducting changes that cascade through multiple modules in our application
- Writing new classes is less likely to introduce problems, as this new classes is no where referenced
Let us see complete code and explanation here
.
Liskov Substitution Principle
The Liskov Substitution Principle states that subtypes must be substitutable for their base types. In order to substitute work, child class must not
- Remove base class behavior
- Violate base class invariants
In general calling code should not know they are different from base types. LSP suggests that IS-A relationship
should be replaced with IS-SUBSTITUTABLE-FOR
.
Let us take example of any web site subscription. Web site offer two types of customer, one is free and other is paid customer. So we will be having an interface like below
public interface ICustomer { string CustomerName { set; get; } int CustomerCode { set; get; } int ProductQuantity { set; get; } double ProductRate { set; get; } CustomerType CustomerType { get; } double GetDiscount(); string PrintInvoice(double _amount); }
Two classes will be created for free and paid customer.
class FreeCustomerSalesEnity:ICustomer { private string _customername; private int _customercode; private int _productquantity; private double _productrate; public string CustomerName { set { _customername = value; } get { return _customername; } } public int CustomerCode { set { _customercode = value; } get { return _customercode; } } public int ProductQuantity { set { _productquantity = value; } get { return _productquantity; } } public double ProductRate { set { _productrate = value; } get { return _productrate; } } public CustomerType CustomerType { get { return CustomerType.Free; } } public double GetDiscount() { return 0; } public string PrintInvoice(double _amount) { throw new NotImplementedException(); } }
public enum CustomerType : int { Free, Paid } public class PaidCustomerSalesEnity : ICustomer { private string _customername; private int _customercode; private int _productquantity; private double _productrate; public string CustomerName { set { _customername = value; } get { return _customername; } } public int CustomerCode { set { _customercode = value; } get { return _customercode; } } public int ProductQuantity { set { _productquantity = value; } get { return _productquantity; } } public double ProductRate { set { _productrate = value; } get { return _productrate; } } public CustomerType CustomerType { get { return CustomerType.Paid; } } public double GetDiscount() { double rate = ProductQuantity * ProductRate; double discountamount = 0; double disrate = 20; discountamount = (disrate / 100) * rate; rate = rate - discountamount; return rate; } public string PrintInvoice(double _amount) { return "Product Invoice For Customer " + CustomerName + " with Total Amount " + _amount; } }
Code to add subscription for both classes.
ICustomer objIcust; List listcust = new List(); objIcust = new WithoutRefector.PaidCustomerSalesEnity(); objIcust.CustomerName = "Paid Customer"; objIcust.CustomerCode = 001; objIcust.ProductQuantity = 5; objIcust.ProductRate = 20; listcust.Add(objIcust); objIcust = new WithoutRefector.FreeCustomerSalesEnity(); objIcust.CustomerName = "Free Customer"; objIcust.CustomerCode = 002; objIcust.ProductQuantity = 5; objIcust.ProductRate = 20; listcust.Add(objIcust); string printinvoice = ""; foreach (ICustomer iCust in listcust) { double amount = iCust.GetDiscount(); printinvoice = iCust.PrintInvoice(amount); // Throw exception for free customer as it is not required there lstCustomerDiscount.Items.Add("Invoice Report –> " + printinvoice); }
Lets refactor above code to comply with Liskov Substitution principal here .
Interface Segregation principle
The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use. ISP splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them. Such shrunken interfaces are also called role interfaces. ISP is intended to keep a system decoupled and thus easier to refactor, change, and redeploy. ISP is one of the five SOLID principles of object-oriented design, similar to the High Cohesion Principle of GRASP. Let us take example of Restaurant where order can be placed in different form.
public interface IOrder { bool acceptOnlineOrder(); bool takeTelephoneOrder(); bool payOnline(); bool walkInCustomerOrder(); bool payInPerson(); }
This interface is implemented by different classes based on order type. Online order class implementation will implement method related to online order.
public class OnlineOrder : IOrder { public bool AcceptOnlineOrder() { return true; } public bool PayInPerson() { throw new NotImplementedException(); } public bool PayOnline() { return true; } public bool TakeTelephoneOrder() { throw new NotImplementedException(); } public bool WalkInCustomerOrder() { throw new NotImplementedException(); } }
Telephone order class implementation will implement method related to Telephone order.
public class TelephoneOrder : IOrder { public bool AcceptOnlineOrder() { throw new NotImplementedException(); } public bool PayInPerson() { throw new NotImplementedException(); } public bool PayOnline() { return true; } public bool TakeTelephoneOrder() { return true; } public bool WalkInCustomerOrder() { throw new NotImplementedException(); } }
InPersonOrder order class implementation will implement method related to InPersonOrder order.
public class InPersonOrder : IOrder { public bool AcceptOnlineOrder() { throw new NotImplementedException(); } public bool PayInPerson() { return true; } public bool PayOnline() { throw new NotImplementedException(); } public bool TakeTelephoneOrder() { throw new NotImplementedException(); } public bool WalkInCustomerOrder() { return true; } }
In all above implementation some methods are not implemented as those are not required for that class. So we should segragate interface to different type.
Below are refactored code.
public interface IOrder { bool PlaceOrder(); }
public interface IPayMoney { bool PayMoney(); }
Now implement each order type classes.
public class OrderOnline : IPayMoney, IOrder { public bool PayMoney() { return true; } public bool PlaceOrder() { return true; } }
Dependency Inversion Principle
According to Wikipedia the Dependency Inversion Principle (popularized by Robert Martin) states that:
- High Level Modules should not be depend upon low level modules. Both should depend upon abstractions.
- Abstraction should not depend upon details. Details should depend upon abstraction.
In application architecture, UI is always on top level. All request goes from UI to business layer and then database layer. Database layer connects with database and fetch required data.
When we define Dependency Inversion Principle in above architectute, The presentation layer defines the abstractions it needs to interact with an business layer and the business layer defines the abstractions it needs from a data access layer. The higher layer defines the abstractions and lower layers implement those abstractions.
The second part of the principle stating that abstractions should not depend upon details rather details should depend upon abstractions. It means that if the details change they should not affect the abstraction. Application should keep working as it was working earlier.
See code explanation here.
Summary
Solid Principle is way to cleaner and managable code. If this is used in proper way than it will help us in maintainable, flexibile, robust, and reusable code.