Single Responsibility Principle (SRP) Open Closed Principle (OCP) Richter Replacement Principle (LSP) Interface Isolation Principle (ISP) Dependency Inversion Principle (DIP)

Solid is an acronym for the five key principles of object-oriented design that make software more robust and stable when we design classes and modules. So what is the SOLID principle? In this article, I’ll talk about the concrete use of the SOLID principle in software development.

The Single Responsibility Principle (SRP) states that a class has one and only one responsibility. A class is like a container; it can add any number of properties, methods, and so on. However, if you try to implement too much of a class, it can quickly become clunky. Any small change will result in a change in this single class. When you change the class, you will need to test it again. If you follow SRP, your classes will be concise and flexible. Each class will be responsible for a single problem, task, or point of concern, in this way you only need to change the corresponding class, and only that class needs to be tested again. The SRP core is to break the whole problem into small pieces, and each small piece will be addressed through a separate class.

Suppose you’re building an application that has a module that searches for customers based on criteria and exports them in Excel. As the business grows, search terms will continue to grow, and so will the categories of exported data. If you put the search and data export functions in the same class at this point, it will become bulky, and even minor changes may affect other functions. So according to the single responsibility principle, a class has only one responsibility, so create two separate classes to handle the search and export data.

The Open Closure Principle (OCP) states that a class should be open for extension but closed for modification. This means that once you create a class and the rest of the application starts using it, you should not modify it. Why is that? Because if you change it, chances are that your change will cause the system to crash. If you need some extra functionality, you should extend the class rather than modify it. Using this approach, the existing system does not see any new changes. In the meantime, you only need to test the newly created class.

Suppose you’re developing a Web application that includes an online tax calculator. Users can visit Web pages, specify details of their income and expenses, and use some mathematical formulas to calculate the tax payable. With this in mind, you create the following class:

public class TaxCalculator

{

public decimal Calculate(decimal income, decimal deduction, string country)
{
    decimal taxAmount = 0;
    decimal taxableIncome = income - deduction;
    switch (country)
    {
        case "India":
            //Todo calculation
            break;
        case "USA":
            //Todo calculation 
            break;
        case "UK":
            //Todocalculation
            break;
    }
    return taxAmount;
}

} This method is very simple, by specifying income and expenditure, you can dynamically switch between different countries to calculate different taxes. But there is an implicit problem here. It only looks at three countries. As this Web application becomes more and more popular, more and more countries will be added and you will have to modify the Calculate method. This violates the Open Closed Principle, and it is possible that your changes will cause the rest of the system to crash.

Let’s refactor this functionality to fit that it is open to extension and closed to modification.

From the class diagram, you can see that horizontal extension is achieved through inheritance without causing modifications to other unrelated classes. The Calculate method in the TaxCalculator class is surprisingly simple:

public decimal Calculate(CountryTaxCalculator obj)

{

decimal taxAmount = 0;
taxAmount = obj.CalculateTaxAmount();
return taxAmount;

}

The Richter Substitution Principle (LSP) states that derived subclasses should be replaceable for base classes, that is, any base class can appear, subclasses must appear. It is worth noting that when you implement polymorphic behavior through inheritance, the system may throw exceptions if the derived class does not comply with the LSP. So be careful with inheritance, and only use it if you’re sure it’s an “IS-A” relationship.

Suppose you are developing a large portal site that provides a lot of custom functionality to end users. The system provides different levels of Settings depending on the level of the user. Considering this requirement, the following class diagram is designed:

As you can see, the ISettings interface has three different implementations: GlobalSettings, SectionSettings, and UserSettings. GlobalSettings Settings affect the entire application, such as title, theme, and so on. SectionSettings applies to various parts of the portal, such as news, weather, sports, and so on. UserSettings Settings for specific logged-in users, such as email and notification preferences.

This design is fine, but if there is another requirement, the system needs to support the visitors, the only difference is that the visitors do not support the system Settings, in order to meet this requirement, you might design as follows:

public class GuestSettings : ISettings

{

Public void getSettings () {// GetSettings from Database // Include guest name, IP address... } public void SetSettings() { //guests are not allowed set settings throw new NotImplementedException(); }

Is that okay? To be precise, there are pitfalls in the system. When using GuestSettings alone, we subconsciously don’t call the setSettings method because we know that visitors can’t be set. However, due to polymorphism, the implementation of the ISettings interface can be replaced with a GuestSettings object, which may throw a system exception when the setSettings method is called.

Refactor this function into two different interfaces: iReadableSettings and iWriteableSettings. Subclasses implement the required interfaces according to the requirements.

The Interface Isolation Principle (ISP) states that classes should not be forced to rely on methods they do not use, that is, an interface should have as few behaviors as possible, that it is concise, and that it is unitary.

Suppose you are developing an e-commerce website that needs a shopping cart and associated order processing mechanisms. You design an interface, IOrderProcessor, that contains a method that validates whether a credit card is valid (ValidateCardInfo) and a method that validates the recipient’s address (ValidateShippingAddress). At the same time, create a class for the OnlineOrderProcessor to represent online payments.

This is great, and your website will work. Now let’s consider another scenario, suppose that online credit card payments are no longer available and the company decides to accept cash on delivery payments. At first glance, this solution sounds simple. You can create a CashonDeliveryProcessor and implement the IOrderProcessor interface. Payment in cash on delivery purchase does not involve any credit card validation, so throw NotImplementedException CashOnDeliveryOrderProcessor class internal ValidateCardInfo method.

Potential problems with such a design may arise in the future. Assume that for some reason online credit card payments require an additional verification step. Naturally, the IOrderProcessor will be modified to include those additional methods, while the OnlineOrderProcessor will implement those additional methods. However, CashOnDeliveryOrderProcessor although does not require any additional features, but you must realize these additional functions. Obviously, this violates the interface isolation principle.

You need to refactor this function:

The new design is divided into two interfaces. The IOrderProcessor interface contains only two methods: ValidateShippingAddress and ProcessOrder, while ValidateCardInfo abstracts to a single interface: IonlineOrderProcessor. Any changes now, online credit card payment is only limited to IOnlineOrderProcessor and its subclasses, and CashOnDeliveryOrderProcessor will not be affected. Therefore, the new design conforms to the interface isolation principle.

The DIP Principle of Dependency Inversion (DIP) states that high-level modules should not depend on low-level modules; instead, they should depend on abstract classes or interfaces. This means that you should not use specific low-level modules within high-level modules. Because of this, the higher-level modules become tightly coupled to the lower-level modules. If tomorrow, you change the lower level module, then the higher level module will also be modified. According to the DIP principle, high-level modules should rely on abstractions (in the form of abstract classes or interfaces), as well as low-level modules. By programming to interfaces (abstract classes), tight coupling is removed.

So what is a high level module and what is a low level module? Typically, we instantiate dependent objects (low-level modules) inside a class (high-level modules), which inevitably creates tight coupling between the two. Any changes to the dependent objects will cause changes to the class.

The Dependency Inversion Principle means that both high level modules and low level modules depend on abstractions. For example, you are developing a notification system that notifies users by email when they change their passwords.

public class UserManager

{

public void ChangePassword(string username,string oldpwd,string newpwd)
{
    EmailNotifier notifier = new EmailNotifier();

    //add some logic and change password 
    //Notify the user
    notifier.Notify("Password was changed on "+DateTime.Now);
}

} Such an implementation is functionally fine, but imagine that the new requirement wants to notify the user in the form of SNS, so we have to manually replace Emainorifier with SNSNotifier. In this case, the UserManager is the high-level module, and the EmailNotifier is the low-level module, and they are coupled to each other. We want to decouple and rely on the abstract INotifier, which is programming to an interface.