Solid principles are a set of principles and guidelines that must be followed to develop a maintainable & reusable software system.
SOLID
- S: Single Responsibility principle
- O: Open/Closed principle
- L: Liskov Substitution principle
- I: Interface Segregation principle
- D: Depedency Inversion principle
S. Single Responsibility principle
- A class should have only one reason to change. This principle allows you to write a single logic inside a single class instead of writing multiple methods with different business logic in the same class.
- This helps to reduce the number of bugs, improves development speed, and, most importantly, makes the developer’s life a lot easier.
- In all the projects, you will find multiple class (.cs) files present with a single responsibility/business logic, which makes the class lighter.
Code
class Marker
{
String name;
string color;
int year;
int price;
public Marker(string name, string color, int year, int price)
{
this.name = name;
this.color = color;
this.year = year;
this.price = price;
}
}
class Invoice
{
private Marker marker;
private int quantity;
public Invoice(Marker marker, int quantity)
{
this.marker = marker;
this.quantity = quantity;
}
public int calculateTotal()
{
int price = ((marker.price) * this.quantity);
return price;
}
public void printInvoice()
{
// Print the Invoice
}
public void saveToDB()
{
// Save the Data into DB
}
}
Explanation
In the above example, we have a class called Marker which will have different methods with different logic. if you observe, that the class is a bit overloaded with multiple business logic methods, it is not a good practice to keep all the methods inside a single class. let's Suppose there is new business logic for the existing methods, so in that case, in all the methods, we have to change the logic.
Problem Statement
- When GST and discount are applied to get the price of the product, then we have to change the existing Logic -- Change the calculation Logic
- If the printing Logic is change
- In case I want to Save into the File instead of the DB, then, in that case, the SavetoDB Logic will be changed.
Resolution
Class Invoice
{
private Marker marker;
private int quantity;
public Invoice(Marker marker,int quantity)
{
this.marker=marker;
this.quantity=quantity;
}
public int CalculateTotal()
{
int price= ((marker.price) * this.quantity);
return price;
}
}
class InvoiceDAL
{
Invoice invoice;
public InvoiceDAL(Invoice invoice)
{
this.invoice = invoice;
}
public void saveToDB()
{
// Save to DB
}
}
class InvoicePrinter
{
private Invoice invoice;
public InvoicePrinter(Invoice invoice)
{
this.invoice = invoice;
}
public void print()
{
// print the invoice
}
}
Explanation
If you see the above code, we have segregated all the business logic into different classes i.e, Invoice (which will contain the Calculation logic method), InvoiceDAL(which will contain SAVE to DB logic method) & InvoicePrinter(which will contain the Printing the invoice logic method). So going forward, if any business logic changes, then only we have to go to the respective class method & change the logic. In this way, we can achieve the Single Responsibility Principle.
Advantages of SRP(Single Responsibility Principle)
When a class has only one responsibility, it becomes easier to change and test. If a class has multiple responsibilities changing one responsibility may impact others & more testing efforts will be required.
O. Open/Closed principle
- Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that a class should be designed in such a way that new functionality can be added without modifying the existing code.
- This principle suggests that the class should be easily extended, but there is no need to change its core implementations. i.e. New features should be implemented using the new code, but not by changing existing code. The main benefit of adhering to OCP is that it potentially streamlines code maintenance and reduces the risk of breaking the existing implementation.
Code
Class Invoice
{
private Marker marker;
private int quantity;
}
class InvoiceDAL
{
Invoice invoice;
public InvoiceDAL(Invoice invoice)
{
this.invoice = invoice;
}
public void saveToDB()
{
// Save to DB
}
}
Note. If there is a new requirement, I need to save it to the file, so in that case, I have to create one more method that will make an insertion of the data into the filesystem. Please find the sample below code for the same.
class InvoiceDAL
{
Invoice invoice;
public InvoiceDAL(Invoice invoice)
{
this.invoice = invoice;
}
public void saveToDB()
{
// Save to DB
}
public void saveToFile(string filename)
{
// Save invoice in the File with the given name
}
}
Explanation
The above approach is not the correct way to write. Instead of writing multiple methods for multiple business requirements, what we can do is we can create a single method & The same method can be kept inside an interface so that it doesn't matter what type of business requirement it is related to saving the data to any File, SQL DB, etc.
If, in the near future, any other scenario comes like I want to save to MongoDB, then, in that case, I have to inherit the interface & implement the logic. So in every case, I don't have to modify the interface. The only thing I have to do is extend the behavior of the method present in the interface.
Resolution
interface IinvoiceDAL
{
public void save(Invoice invoice);
}
Class SQLDBInvoiceDAL : IinvoiceDAL
{
public override void save(Invoice invoice)
{
// Save to SQL DB
}
}
Class FileInvoiceDAL : IinvoiceDAL
{
public override void save(Invoice invoice)
{
// Save to file
}
}
Class MongoDBInvoiceDAL : IinvoiceDAL
{
public override void save(Invoice invoice)
{
// Save to Mongo DB
}
}
L. Liskov Substitution Principle
- The Liskov Substitution Principle says that the object of a derived class should be able to replace an object of the base class without bringing any errors in the system or modifying the behavior of the base class. That means child class objects should be able to replace parent class objects without compromising application integrity.
- Each subclass must maintain all behavior from the base class along with any new behaviors unique to the subclass. The child class must be able to process all the same requests and complete all the same tasks as its parent class.
- LSP is a fundamental principle of SOLID Principles and states that if any of the modules are using a base class, then the derived class should be able to extend its base class without changing its original implementation.
Code
namespace SOLID_PRINCIPLES.LSP
{
class Program
{
static void Main(string[] args)
{
Fruit fruit = new Orange();
Console.WriteLine(fruit.GetColor());
fruit = new Apple();
Console.WriteLine(fruit.GetColor());
}
}
public abstract class Fruit
{
public abstract string GetColor();
}
public class Apple : Fruit
{
public override string GetColor()
{
return "Red";
}
}
public class Orange : Fruit
{
public override string GetColor()
{
return "Orange";
}
}
}
Explanation
In the above example, we have created an abstract class, Fruit which will contain 1 abstract method, i.e GetColor(). Now any derived class i.e (Apple or Orange) will inherit the abstract class, override the abstract method & implement their own logic.
In the case of the Apple derive class, we are overriding the abstract method & return the color as Red, whereas in the Orange derive class, we are overriding the abstract method & return the color as Orange.
Now, any subtype/Derive class (Apple or Orange) of the Fruit class can be replaced with the other subtype without error because of the behavior of the method GetColor(). As a result, this program achieves the LSP principle.
I. Interface Segregation principle
- The interface should be such that clients shouldn't implement unnecessary functions they don't need. Based on the business requirement, we can create multiple interfaces & consume the same wherever it is required.
- We can take an example of a restaurant system.
Code
interface IRestaurantEmployee
{
void WashDishes();
void ServeCustomers();
void CookFood();
}
class Waiter : IRestaurantEmployee
{
public void WashDishes()
{
// Not My Job
}
public void ServeCustomers()
{
// Yes, and here is my implementation
Console.WriteLine("Serving the customer");
}
public void CookFood()
{
// Not My Job
}
}
In the above code, we have one interface with 3 methods. In a restaurant system, we have different departments, i.e, Chef, Waiter, Cleaning Staff, etc. As per the above example waiter class inherited the Interface, i.e IRestaurantEmployee & implemented the waiter-related task, but if you observe for a waiter washDishes() & cookFood() method is not a relevant task. So this is not a valid approach. So to overcome this problem, what we can do is we can create multiple interfaces with the proper method so that we will not face the same problem again.
Resolution
interface IWaiterInterface
{
void ServeCustomers();
void takeOrder();
}
interface IChefInterface
{
void cookFood();
}
interface ICleaningStaffInterface
{
void cleanUtensil();
}
class Waiter : IWaiterInterface
{
public void ServeCustomers()
{
Console.Writeline("Serving the Customer");
}
public void takeOrder()
{
Console.Writeline("Taking the Orders");
}
}
class Chef : IChefInterface
{
public void cookFood()
{
Console.Writeline("preparation of the Food for the Customer");
}
}
Explanation
If you observe to resolve the issue, we have created multiple interfaces IWaiterInterface, IChefInterface & ICleaningStaffInterface, with relevant methods so that the derive class can inherit the interfaces & implement the methods. Waiter class inherited IWaiterInterface. Similarly, the Chef class inherited the IChefInterface interface & implemented the respective methods & so on. Hence it is recommended to create multiple interfaces with multiple methods as per the business requirement so that we can uniquely distinguish & consume the respective interfaces whenever it is required.
As a result, this program achieves the ISP(Interface Segration Principle).
D. Dependency Inversion Principle
- The principle says that high-level modules should depend on abstraction, not on the details, of low-level modules. In simple words, the principle says that there should not be a tight coupling among components of software, and to avoid that, the components should depend on abstraction.
- The terms Dependency Injection (DI) and Inversion of Control (IoC) are generally used interchangeably to express the same design pattern.
- Inversion of Control (IoC) is a technique to implement the Dependency Inversion Principle in C#. Inversion of control can be implemented using either an abstract class or interface. The rule is that the lower-level entities should join the contract to a single interface, and the higher-level entities will use only entities that are implementing the interface. This technique removes the dependency between the entities.
Note. In the below implementation, I have used interface as a reference, but you can use abstract class or interface as per your requirement.
Implementation
In the below code, we have implemented DIP using IoC using an injection constructor. There are different ways to implement Dependency injection. Here, I have used injection through the constructor, but you can inject the dependency into the class's constructor (Constructor Injection), set property (Setter Injection), method (Method Injection), events, index properties, fields, and basically any members of the class which are public.
Code
public class BusinessLogicLayer
{
private readonly IRepositorylayer RL;
public BusinessLogicLayer(IRepositorylayer repositorylayer)
{
RL=repositorylayer;
}
public void Save(object details)
{
RL.Save(details);
}
}
public interface IRepositorylayer
{
void Save(object details);
}
public class DataAccessLayer : IRepositorylayer
{
public void Save(object details)
{
Console.WriteLine("perform the Save operation");
}
}
Advantages of the SOLID Principle
- Improved maintainability: You can create code that is easier to maintain and modify over time because the SOLID principles encourage the creation of modular, flexible code that is less prone to errors and more resistant to changes in requirements.
- Reduced complexity: The SOLID principles help to reduce the complexity of software by promoting the use of abstraction and encapsulation, which can make it easier to understand and work with the code.
- Enhanced flexibility: These principles encourage the creation of flexible code that is open to extension but closed to modification, which encourages flexibility without breaking existing functionality.
- Reusability: SOLID principles promote the creation of reusable components. Well-designed code that adheres to SOLID principles can be easily integrated into different projects or reused within the same project.
- Reduced Bugs and Regression: SOLID principles encourage stable and predictable behaviors. This reduces the likelihood of introducing bugs when making changes or adding new features.
- Parallel Development: The Parallel Development of an application is one of the most important key aspects. As we know, it is not possible to have the entire development team work on the same module at the same time. So we need to design the software in such a way that different teams can work on different modules of the project.