SOLID principles: Introduction, Best Practices, and Interview Questions and answers

By | July 15, 2020

SOLID principle introduction

Following practical and clear design principles is conducive to flexible and reliable software design. SOLID is an acronym for the five essential principles of object-oriented design. When we design classes and modules, following the SOLID principles can make the software more robust and stable.

Q1: What are the SOLID principles?

  • S - Single responsibility principle
  • O - Open/close principle
  • L - Liskov substitution principle
  • I - Interface isolation principle
  • D - Dependency Inversion Principle

Q2: What is the Single Responsibility Principle (SRP)

The Single Responsibility Principle (SRP) seems to be the simplest one. However, it is one of the most confusing principles to follow. Its definition is straightforward: there should be only one reason to refactor a class. The reason for the change indicates the responsibility of this class. It may be either a function in a specific field or a solution to business logic.

This principle indicates that we shouldn't let a class bear too many responsibilities (functionalities). Once there are too many features implemented in one class, it can be really hard to refactor this class due to one specific responsibility, i.e., violating SRP. In this case, the state of the software system is unstable, and any minor modification is likely to affect other functions in this class. Therefore, we need to encapsulate different responsibilities in different classes, that is, encapsulate various reasons for changes in different classes, and changes among different classes do not affect each other.

Let us consider a scenario, there is a class for editing and printing reports. There are two reasons for such a change: First, the report's content can be changed (edited). Second, the format of the report can be changed (printed). If there is a modification to the report editing process, and the report editing process will result in changes in the public state or dependencies, in this case, the code of the printing function will not work. So the single responsibility principle guarantees that the reason for these two changes is two separate functions, which should be separated into different classes.

In the face of a program code that violates the principle of single responsibility, we can use the appearance mode, proxy mode, bridge mode, adapter mode, and command mode to reconstruct the existing design to achieve the separation of multiple responsibilities.

The single responsibility principle is used to control the granularity of the class and reduce the code coupling of unrelated functions in the class, making the class more robust; besides, the single responsibility principle is also applicable to the decoupling between modules, which has a significant impact on the division of the functions of the modules Guiding significance.

Q3: Describe the Open and Closed Principle (OCP)

The Open-Closed Principle (OCP) is called the Open-Closed Principle in English. The basic definition is that the objects (classes, modules, functions, etc.) in the software should be open for extension, but closed for modification. Open to extension here means that by adding new code, the program's behavior can be extended to meet the changes in demand; closed to modify means that the existing code should not be modified when the program's behavior is extended to avoid affecting the original function.

The key to changing the behavior of the system without changing the code is abstraction and polymorphism. Interfaces or abstract classes define the abstraction layer of the system and then expanded by concrete classes. In this way, there is no need to make any changes to the abstraction layer, only need to add a new real class to implement new business functions, to meet the requirements of the opening and closing principle.

Similarly, take an example to understand the opening and closing principles more deeply: there is a Display class for chart display, which can draw various types of charts, such as pie charts, bar charts, etc.; and when you need to draw a specific chart, all Strongly dependent on the corresponding type of chart, the internal implementation of the Display class is as follows:

public void display(String type) {
    if (type.equals("pie")) {  
      PieChart chart = new PieChart();  
      chart.display();  
    }  else if (type.equals("bar")) {  
      BarChart chart = new BarChart();  
      chart.display();  
    } 
}


Based on the above code, if you need to add a chart, such as a line chart LineChart, you must modify the Display() method of the Display class to add new judgment logic. This approach violates the opening and closing principle. The way to implement the class conforms to the open and closed principle is to introduce the abstract chart class AbstractChart, as the base class of other charts, let Display depend on this abstract chart class AbstractChart, and then use Display to decide which specific chart class to use, the implementation code becomes Like this:

private Abstractchart chart;

public void display() {
    chart.display();  
}

Now we need to add a line chart display, inject a LineChart object into the Display on the client, without modifying the source code of the existing class library.

Q4: Describes the Liskov substitution principle (LSP)

Based on not affecting the program's correctness, all places where the base class is used can be replaced with objects of its subclasses. The base class and subclass mentioned here are two types of objects with inheritance relationships. When we pass a subtype object, we need to ensure that the program does not change the behavior and state of any original base class, and the program can operate normally.


To understand the principle of Liskov substitution, here is a classic example that violates the principle of Liskov substitution: the square/rectangular problem.



The picture above shows the class hierarchy of the square/rectangle problem. The Square class inherits the Rectangle class, but the width and height of the Rectangle class can be modified separately, but the width and height of the Suqare class must be modified together. If the User class operates the Rectangle class, but the actual object is of the Suqare type, it will cause an error in the program, as shown in the following code block:

Rectangle r = ...;
r.setWidth(5);
r.setHeight(2);
assert(r.area() == 10);

When the object of the specific type returned is Suqare, the assertion with an area of ​​10 is a failure, which is not in line with the principle of Liskov substitution.

To make the program code conform to the principle of substitution in style, you need to ensure that when the subclass inherits the parent class, in addition to adding new methods to complete the new functions, try not to rewrite the parent class method, in other words, the subclass can extend the parent class Function, but cannot change the original function of the parent class.

On the other hand, the principle of Liskov substitution is also a supplement to the principle of opening and closing. It is not only applicable to the inheritance relationship but also the design of the realization relationship. The IS-A relationship often mentioned is for the behavior mode. If two classes The way of behavior is incompatible, then you should not use inheritance, a better approach is to extract the public part of the method instead of inheritance.

Q5: Describe the Interface Isolation Principle (IIP)


The Interface Segregation Principle (ISP) is called the Interface Segregation Principle in English. The basic definition is that clients should not rely on interfaces that it does not need. The client should only rely on the methods it uses because if an interface has several methods, it means that its implementation class must implement all interface methods, which is very bloated from the code structure.

Now we look at the next example that violates the principle of interface isolation. From the above class structure diagram, multiple users need to operate the Operation class. If User1 only needs the operation1 method, User2 only needs the operation2 method, and User3 only needs the operation3 method, then it is obvious to User1 that you should not see the two methods operation2 and operation3. The dependence, to prevent the modification of operation2 and operation3 methods in the Operation class, affect the function of User1. This problem can be solved by isolating different operations into independent interfaces, as shown in the following figure.

Based on the principle of interface isolation, all we need to do is to reduce the definition of an extensive and complete interface. The interface to be implemented by the class should be decomposed into multiple interfaces, and then implemented according to the required function, and where the interface method is used, use the corresponding The interface type is declared, so that the caller and the object's non-related methods can be released from the dependency relationship. To summarize, the interface isolation principle's primary function is to control the granularity of the interface, prevent exposure to the client without relevant code and methods, ensure high cohesion of the interface, and reduce coupling with the client.

Q6: What is the Dependency inversion principle (DIP)


Dependency Inversion Principle (DIP) Dependency Inversion Principle (DIP), the basic definition are as follows:

  • High-level modules should not depend on low-level modules, but should rely on abstractions together;
  • Abstraction should not depend on details. Details should depend on abstractions.
  • The abstract here is the interface and the abstract class, and the details are the class generated by implementing the interface or inheriting the abstract class.


How to understand that "high-level modules should not depend on low-level modules, but should rely on abstraction together"? If the high-level module depends on the low-level module, the low-level module's change is likely to affect the top-level module, resulting in the high-level module being forced to change, which makes it difficult to reuse the high-level module.


The best practice is to create a stable abstract layer in the high-level module, and only rely on this abstract layer; the bottom layer module completes the implementation details of the conceptual layer. In this way, high-level classes use the next layer through this abstract interface, removing the high-level dependence on the underlying implementation details.

Regarding the dependency inversion principle, the available design patterns are factory pattern, template method pattern, and strategy pattern.

Relying on the principle of inversion can reduce the coupling between classes, improve the system's stability, reduce the risks caused by parallel development, and improve the readability and maintainability of the code. Simultaneously, the principle of reliance on inversion is also the core principle of framework design. It is good at creating reusable frameworks and extensible code, such as Servlet specification implementation of Tomcat container and Spring Ioc container implementation.