preface

SOLID is the five basic principles of object-oriented design, including

SRP Single responsibility principle
OCP The open closed principle
LSP Richter’s substitution principle
ISP Interface isolation Rule
DIP Dependency reversal principle

These principles guide how to organize functions, classes, and other code so that programs can be more readable, modified, and reused to improve maintainability and extensibility. These principles are described in turn

Single responsibility principle

The Single Responsibility Principle says that

Any module should be responsible for only one type of behavior

Here is an example

There is a Person class that has three behaviors that are responsibilities for three different positions

  • Setting corporate strategy: belongs to the CEO
  • Management of human resources: belongs to HRD
  • Management technology: belongs to CTO

While these can all be ascribed to the behavior of one person, their applicable scenarios and business requirements can vary widely, and coupling them to a class or module violates SRP principles. If the logic of one post is modified, it will easily affect the logic of other posts. Even if the other two positions remain unchanged, regression testing is needed to be on the safe side. To avoid this problem, you need to break up your code so that a class or module is only responsible for one thing.

In the low-level coding implementation, the SRP principle is reflected as a function does only one thing, which is often used when refactoring and refining functions

The open closed principle

The Open Closed Principle says

Objects (classes, modules, functions, etc.) in software are open for extension, but closed for modification ****

Software that meets this requirement can modify the original requirement with little or no modification

When designing classes and modules, we can rely on interfaces or abstract classes for coding. When we need to extend the implementation, we can complete the function extension without modifying or only modifying the code of a few callers. Its significance is to reduce the modification and increase the system stability

At the software architecture level, this principle also makes sense

In a good software architecture, you iterate, changing as little code as possible

One way to do this is to identify the stable parts of the business as the core layer. Let the periphery of the variation depend on the core layer as the business layer

The core layer is stable for two reasons

  • Dependencies: The core layer has no business dependencies, so very few dependency changes cause the core layer to change
  • Dependent party: On the other hand, the core layer is dependent on several external business layers. Therefore, if the core layer wants to change, the compatibility of the business layer needs to be considered, making the change of the core layer very careful. Conversely, changes in the business layer usually do not affect the core layer

The business layer is also prone to change for two reasons

  • Dependencies: As long as the business layer’s dependencies change, the business layer must adapt to the change. It is currently possible to improve stability by relying on the core layer’s interface rather than the implementation, referring to the DIP principle
  • Business requirements: During business iterations, adjustments to the business logic are required, usually changing the business layer rather than the core layer

Thus, the core layer is the one that satisfies the OCP principle the most because it rarely changes. The business layer is variable, but its impact is limited and does not affect the core layer. In the architecture, the unchanged part is put into the inner layer, and the easy to become part is put into the outer layer. The outer layer depends on the inner layer, which can realize the minimum change of the whole each time the modification is made

Richter’s substitution principle

The Liskov Substitution principle says

Derived class (subclass) objects can replace their base class (superclass) objects in a program

At the code level, this principle is used as a guide to how inheritance is implemented

If a subclass implements an interface, it must correctly implement the methods it defines. If all subclasses meet this requirement, they can replace the base class in their code

From the point of view of the user of the interface, subclasses that implement the interface must satisfy the definition of the interface. The benefit is that when subclass implementations are replaced on subsequent extensions, no special processing is required to consider whether the new implementation class meets the business requirements

This may sound abstract, but here is an example that does not satisfy the LSP principle

  • The Set interface: defines the add and length methods. The length method returns the number of distinct elements in the Set
  • Array implementation class: implements the Set interface, but does not implement the length method of the interface. Instead, it returns the number of all elements

If the user gets the interface code, it must write the following test

Set set = ...

set.add(1)

set.add(1)

set.add(2)

assert(set.lengh() == 2)
Copy the code

As defined by the Set interface, the lengh() function returns the number of deduplicated interfaces. But if the subclass that implements the interface does not satisfy the definition of set, that is, does not implement the interface correctly, then the final assert will certainly not hold. The type of subclass can be determined at the caller and special processing can be done, but this cannot achieve the purpose of subclass substitution, which violates the LSP principle. Therefore, it is best to make the subclass implement the superclass or interface correctly

The Richter replacement principle is a principle to be followed in the implementation of inheritance. Following the Richter replacement principle, the caller changes less when the business is expanded, and there is no need to consider too many compatibility problems, thus increasing the stability of the system

Interface isolation Rule

The Interface isolation Principle refers to the following

Users should not be forced to use methods or features that are not useful to them

In general, a module that relies on functionality it doesn’t need can cause problems

There is a mobile phone interface, which has two functions: call() and play 2D game playGame()

Ordinary people only use it for making calls, professional players only use it for making calls and playing 2D games

While the average person doesn’t need to play the game features, they form dependencies at the source code level. This means that if a new 3D game feature is added in the future, although it has nothing to do with ordinary people, ordinary people also need to feel the change, which may cause unnecessary modifications and reduce the stability of the system

It makes sense to split the Phone into two interfaces

In this way, the average person doesn’t need to know what features are available to play the game to satisfy the least knowledge principle. Secondly, ordinary people do not have to perceive any changes related to playing games, so they do not have to make corresponding modifications to improve the stability of the system

Dependency reversal principle

The Dependency Inversion Principle refers to

Try to rely on the abstract interface rather than the concrete implementation in the source code dependencies

When we change the interface layer, we will definitely change the implementation layer. Conversely, if we change the implementation layer, we will most likely not change the interface layer. The interface layer is more stable than the implementation layer. Therefore, if it depends on the interface layer, the probability of change is small, so it is relatively stable.

If the interface layer is carefully designed to meet the current and future business needs, the system stability can be further improved

It is impossible to fully satisfy this principle in software design. For example, all the dependent class libraries, including String, Thread and other classes, are concrete implementations. But this standard class is usually so stable that we don’t have to worry about it being modified, we should focus on the more volatile business layer

Some design patterns follow this principle, such as:

  • Factory method pattern: Create products using abstract factories to facilitate subsequent replacement of concrete factory implementations
  • Template method pattern: users rely on abstract superclasses, and subclasses implement specific abstract methods for easy replacement
  • Policy mode: Users rely on abstract policy interfaces to facilitate policy replacement

Following the dependency reversal principle, the essence is to rely on more stable modules, increasing the stability of the current module

conclusion

This article describes in detail the concept of SOLID’s five design principles and what they can achieve in terms of code and architecture. It recommends that you use these design principles when designing, writing, and refactoring code to make the code more readable and the system more stable

Reference documentation

Wikipedia -SOLID principles