SOLID PRINCIPLE – The actual solid base of your code.
What are SOLID PRINCIPLES? Why are they considered so effective for CODE BETTERMENT? Why are they the most commonly asked questions in INTERVIEW PROCESS?
These are some of the very simple yet very tempting questions we see rotating around the concept of solid principles. So, to answer all those questions, Lets quickly dive into it…
S.O.L.I.D
S — The Single Responsibility Principle
O — The Open Closed Principle
L — The Liskov Substitution Principle
I — The Interface Segregation Principle
D — The Dependency Inversion Principle
S: The Single Responsibility Principle
As clearly stated by the name, each and every class wrote should be responsible for one specific work.
Let’s take an example of your company where instead of a specific team of HR, Operations, Technical, all you have is a bunch of people doing everything all together. HR handling technical, technical team handling employee leaves, operations connecting with the client for requirement gathering etc.. One word for this situation is “Boo-Boo”.
Same is with your code, To make your classes more independent and focused on one work you need to follow “S” part of SOLID principles.
let me give you a basic example to start with :
class ClassNotSoResponsible
{
public static void DisplayData()
{
Console.WriteLine("Data Displayed successfully");
Console.WriteLine("Logged Information");
}
}
Now in the above example, your “ClassNotSoResponsible” works on two aspects — displaying data (it’s actual work) and Logging data (it’s overhead work). Now let’s see what “ClassAbsolutelyResponsible” will look like.
class ClassAbsolutelyResponsible
{
public static void DisplayData()
{
Console.WriteLine("Data Displayed successfully");
var logData = "Logged Information";
ClassAbsolutelyResponsibleForLogging.LogData(logData);
}
}class ClassAbsolutelyResponsibleForLogging
{
public static void LogData(String info)
{
Console.WriteLine(info);
}
}
Did you spot the difference? Now someday if you have to change the way you log data actually functionality of the class is not hindered.
O: The Open Closed Principle
Open Closed Principle is very accurately defined in a single line.
“ A software module/class is open for extension and closed for modification”
Which in simple words means that you create your class in such a way that the new functionality can be added only when new requirements are generated — “OPEN FOR EXTENSION”.
Second part of phrase means if we have a class up and running it is disturbed only in the cases of big fixes — “CLOSED FOR MODIFICATION”.
Let’s start with an example, Suppose you have a class called as Rectangle with certain properties, and another class called Area calculator that calculate area of the rectangle.
public class Rectange
{
public double Width { get; set; }
public double Height { get; set; }
}
public class AreaCalculator
{
public double Area(Rectangle[] shapes)
{
double area = 0;
foreach (var shape in shapes)
{
area += shape.Width*shape.Height;
}
return area;
}
}
Now suppose, you have to calculate area not only for rectangles but also for circles, NOW WHAT?? Above code needs a lot of changes on that background.
Which is a complete disaster in this case as one bug/mistake during code modification can affect the existing code base as well. Which makes “AreaCalculator” completely opposite to what Open-closed principle states. Hence, the best way to do the above problem is stated below :
Look for “Full Abstraction or Partial Abstraction” as per your requirement. In this case, we can create an Interface called “Shape”, which will be further inherited by Rectangle, Circle or any many classes that require this functionality. We will simply Declare a function called “AreaCalculator” in Interface and Leave definition onto the classes inheriting the Interface. Have a look at the example below :
public abstract class Shape
{
public abstract double Area();
}public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double Area()
{
return Width*Height;
}
}public class Circle : Shape
{
public double Radius { get; set; }
public override double Area()
{
return Radius*Radius*Math.PI;
}
}
Now, This completely satisfies the “Open-Closed Principle”.
L: The Liskov Substitution Principle
Functions that use pointers to base classes must be able to use objects of derived classes without knowing it.
In simple words, if You have a Group of people working as — StarWorkers, SilverWorkers and Gold Workers working as “Employee” where employee class holds Bonus and StarBonus. Now according to the company all belong to the employee, will have Bonus but on StarEmployee will have StarBonus. Have a look at the bottom code.
class Employee
{
public virtual GetBonus()
{
return 100;
}
public virtual GetStarBonus()
{
return 500;
}
}class StarWorkers : Employee
{
public override GetBonus()
{
return 200;
} public virtual GetStarBonus()
{
return 1000;
}
}class SilverWorkers : Employee
{
public override GetBonus()
{
return 200;
}
}
Now, clearly if run below set if statements Exception occurs as SilverWorker won’t have GetStartBonus().
List<Employee> Employees = new List<Employee>();
Employees.Add(new StarWorkers());
Employees.Add(new SilverWorkers());
foreach (Employee o in Employees)
{
o.GetStarBonus(); //throw exception
}
Because SilverWorkers are not exact SubClass of the Employee Class, the best way to achieve above is to have 2 classes — Employee and StarEmployee. Where Employee is inherited by StarWorkers and SilverWorkers both. whereas, StarEmployee is only inherited by StarWorkers. So, the actual code becomes as below:
class Employee
{
public virtual GetBonus()
{
return 100;
}
}class StarEmployee
{
public virtual GetStarBonus()
{
return 500;
}
}class StarWorkers : Employee, StarEmployee
{
public override GetBonus()
{
return 200;
} public override GetStarBonus()
{
return 1000;
}
}class SilverWorkers : Employee
{
public override GetBonus()
{
return 200;
}
}
I: The Interface Segregation Principle
As simple as the name, it states — classes should not be forced to implement any interface that they actually don’t use. Instead of one overloaded Interface where multiple functions with different functionality are defined, it’s better to have multiple small interfaces separated on their use and type of functionality they will perform.
Let’s examine the example below – we have an interface called as – “WorkerAtSuperMart”. Now this interface has 3 methods declared– DoCleaning(), MaintainInventory(), and AttendCustomer(). Now look at the code below :
Interface WorkerAtSuperMart
{
public void DoCleaning();
public void MaintainInventory();
public void AttendCustomer();
}class Worker1 : WorkerAtSuperMart
{
public void DoCleaning()
{
//Does Cleaning
} public void MaintainInventory()
{
//Not my work
}
public void AttendCustomer()
{
//Not my work
}
} class Worker2 : WorkerAtSuperMart
{
public void DoCleaning()
{
//Does Cleaning
} public void MaintainInventory()
{
//Maintains Inventory
}
public void AttendCustomer()
{
//Attends Customers
}
}
In the above example Worker1 only works on cleaning and worker2 works on Cleaning, maintaining inventory and attending customers as well. Now as my both the workers implement one single interface “WorkerAtSuperMart” they are forced to use functions they don’t even use.
Now, what would be a better approach to this? Look at the code sample below :
Interface CleaningWorkerAtSuperMart
{
public void DoCleaning();
}
Interface InventoryWorkerAtSuperMart
{
public void MaintainInventory();
public void AttendCustomer();
}class Worker1 : CleaningWorkerAtSuperMart
{
public void DoCleaning()
{
//Does Cleaning
}
}class Worker2 : CleaningWorkerAtSuperMart,InventoryWorkerAtSuperMart
{
public void DoCleaning()
{
//Does Cleaning
}public void MaintainInventory()
{
//Maintains Inventory
}
public void AttendCustomer()
{
//Attends Customers
}
}
D: The Dependency Inversion Principle
Dependency inversion principle states that no high-level modules should be dependent on low-level modules. They should interact to or should be dependent on proper abstractions. Where high-level modules deal with business logic whereas low-level modules work more closely to actual and detailed functionality.
More detailed description would be :
Robert C. Martin’s definition of the Dependency Inversion Principle consists of two parts:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
Now let’s move towards example to be more descriptive:
Let’s assume we have an eCommerce website which we just started as an early stage website. so we decided that our website will only work on CASH/COD system. so, on an upper level our code architecture looks like so
public class Cash {
public PayCash()
{
//Pay CASH
}
}public class Payment {
public PayThroughCash()
{
Cash x = new Cash();
x.PayCash();
}
}
Now, as we were growing we decided to include CARD payment as well. so our structure changed to
public class Cash {
public PayCash()
{
//Pay CASH
}
}public class Card {
public PayByCard()
{
//Pay CARD
}
}public class Payment {
public PayThroughCash()
{
Cash x = new Cash();
x.PayCash();
}
public PayThroughCard()
{
Card x = new Card();
x.PayByCash();
}
}
Did, you spot the issue, Yes every time any new payment method is selected my code needs modification not only at the base class where we describe the card functionality but also at the Payment class.
Now, what would be the best way to achieve this. Have a look at the example below.
Interface IPaymentMethod
{
public void Pay ();
}public class Cash : IPaymentMethod
{
public Pay()
{
//Pay CASH
}
}public class Card : IPaymentMethod
{
public Pay()
{
//Pay CARD
}
}public class PaymentMethod {
private IPaymentMethod paymentMethod;
public PaymentMethod (IPaymentMethod _paymentMethod)
{
this.paymentMethod = _paymentMethod;
}
public void ExecutePayment(){
paymentMethod.Pay();
}
}public Class Payment{
public void TakePayment()
{
// if pay from cash
PaymentMethod _paymentMethod = new PaymentMethod(new Cash());
_paymentMethod.Pay(); //if card
PaymentMethod _paymentMethod = new PaymentMethod(new Card());
_paymentMethod.Pay(); }}
If you carefully go through these principles, you will notice there is nothing new or complex the only difference is the way they are used or you can say architecture of the code.
I hope, I made these principles as easy as possible to understand. if Iwent wrong at any place or any area that needs improvement, Please leave your valuable comments so I can work on loopholes and make this article much better!
Thank You! Enjoy making your code more SOLID!!