While SOLID is universally recognized as an important design principle, and every single one of them is familiar, I find that most developers don’t really understand it. To get the most out of them, you must understand their relationships and apply all of them together. Only with SOLID as a whole is it possible to build SOLID software. Unfortunately, we see books and articles that list each principle without looking at them as a whole, and even Uncle Bob, who came up with SOLID principles, doesn’t get it right. So LET me try to explain my understanding.

Here’s my point: A single responsibility is the foundation of all design principles, and the open closed principle is the ultimate goal of design. The Richter substitution principle emphasizes the correctness of the program runtime after a subclass replaces its parent class. It is used to help implement the open close principle. The interface isolation principle is used to help implement the Richter substitution principle, and it also embodies a single responsibility. Dependency inversion is the principle that separates procedural programming from OO programming, and it is also used to guide the principle of interface isolation. The relationship is shown as follows:

Single Responsibility Principle

Single responsibility is one of the easiest design principles to understand, but also one of the most violated.

It is not so easy to understand and properly apply the single responsibility principle. A single responsibility is like a pinch of salt. Robert C. Martin (aka “Uncle Bob”) defined responsibility as the cause of change, describing A single responsibility as “A class should have only one reason to change.” That is, if there are multiple reasons for A class to change, This class then violates the single responsibility principle. So what are the “causes of change”?

The role of stakeholders is an important reason for change, and different roles will have different needs, resulting in different reasons for change. As residents, the household wire is ordinary 220V wire, and the grid builders, the use of high voltage wire. Having a Wire class serving two roles at the same time is usually a bad taste.

Frequency of change is another reason to consider change. Even within the same class of roles, the frequency of requirements change can vary. The most typical example is that business processing needs are more stable, while business presentation needs are more likely to change. After all, people like new things. Therefore, these two types of requirements are usually implemented in separate classes.

The single responsibility principle sort of separates the concerns. Separate the concerns of different roles, separate the concerns of different times.

How do you apply the single responsibility principle in practice? When to split and when to merge? Let’s see how a new chef learns to cook with a pinch of salt. He would taste it until it was just right. The same goes for writing code. You need to recognize the signs of changing requirements, keep “tasting” your code, and when the “taste” isn’t good enough, keep refactoring until the “taste” is just right.

The Open-closed Principle

The open closed principle states that software entities (classes, modules, etc.) should be open for extension but closed for modification. Now that sounds pretty irrational, you can’t modify it, you can only extend it, right? So how do I write code?

Let’s see why we have the on/off principle. Let’s say you are a successful open source library author, and many developers use your library. If one day you want to extend functionality, you can only do so by modifying some code, which will result in all users of the library having to change their code. Even worse, the changes they are forced to make may cause other dependencies to change their code as well. This scenario is an absolute disaster.

If your design is open and closed, it’s a completely different scenario. You can change the behavior of your software by extending it, not modifying it, to minimize the impact on dependent parties.

Isn’t that the ultimate goal of design? Aren’t the design principles of decoupling, high cohesion, low coupling, and so on all geared toward this end? Imagine that classes, modules, and services do not need to be modified, but can be extended to change their behavior. Just like a computer, components can be easily expanded. Hard drive too small? Just get a bigger one. The monitor isn’t big enough? How about an 8K?

When should you apply the open close principle, and how? No one can identify all extension points at the outset, nor can they be reserved everywhere, and the cost of doing so is unacceptable. So it must be driven by a change in demand. If you have the support of a domain expert, he can help you identify the points of change. Otherwise, you should make the decision as the change happens, because it is against Yagni to do too much upfront design without any basis.

The key to implementing the open close principle is abstraction. In the days of Bertrand Meyer’s open-close principle (the 1980s), adding properties or methods to a class library inevitably changed the code that relied on that library. This obviously makes the software difficult to maintain, so his emphasis is on allowing classes to be extended through inheritance. As technology evolves, there are more ways to implement the open closed principle, including interfaces, abstract classes, policy patterns, and so on.

We may never fully achieve the open and close principle, but it is the ultimate goal of design. Other SOLID principles, such as the Richter substitution principle, are directly or indirectly in service of the open close principle.

The Liskov Substitution Principle

Richter’s substitution principle states that derived (subclass) objects can be used instead of their base (superclass) objects. Those of you who have studied OO know that subclasses can replace superclasses, so why the Richter substitution rule? The emphasis here is not on compilation errors, but on correctness when the program runs.

Programs that run correctly can generally be divided into two categories. One kind is can’t appear a runtime exception, the most typical is UnsupportedOperationException, namely subclass does not support the superclass method. The second category is business correctness, which depends on the business context.

In the following example, because java.sql.Date does not support the toInstance method of the parent class, when the parent class is replaced by it, the program cannot run properly, breaking the contract between the parent class and the caller, and thus violating the Richter substitution principle.

package java.sql;

public class Date extends java.util.Date {
    @Override
    public Instant toInstant() { throw new java.lang.UnsupportedOperationException(); }}Copy the code

Let’s look at examples of business correctness violations. The most typical example is the square inheriting the rectangle described by Uncle Bob in Agile Software Development: Principles, Patterns, and Practices. In a general sense, a square is a rectangle, but this inheritance destroys the correctness of the business.

public class Rectangle {
    double width;
    double height;

    public double area() {
        return width * height;
    }
}

public class Square extends Rectangle {
    public void setWidth(double width) {
        this.width = width;
        this.height = width;
    }

    public void setHeight(double height) {
        this.height = width;
        this.width = width;
    }
}

public void testArea(Rectangle r) { r.setWidth(5); r.setHeight(4); assert(r.area() == 20); / /! If r is a square, the area is 16}Copy the code

If the testArea method argument in the code is a square, the area is 16, not 20 as expected, so the result is obviously incorrect.

If your design meets the Richter’s substitution principle, then subclasses (or the implementation class of the interface) can replace the parent class (or interface) as long as it is correct, changing the behavior of the system and thus extending. BranchByAbstraction and the strangler model are both based on Richter’s substitution principles for system expansion and evolution. This is closed to modification and open to extension, so the Richter substitution principle is a solution to the open-closed principle.

And in order to do Richter’s substitution, you need interface isolation.

Interface Segregation Principle

The interface isolation principle states that a client should not be forced to rely on methods it does not use. Simply put, a smaller and more specific thin interface is better than a big, fat, fat interface.

Responsibility too much fat interface, very easy to violate the single responsibility principle, can also lead to implementation classes have to throw UnsupportedOperationException such abnormalities, in violation of the magnitude of the substitution principle. Therefore, the interface should be designed to be thinner.

How to lose weight for the interface? Interfaces exist for decoupling. Developers often make the mistake of thinking that implementing classes requires interfaces. It is the consumer who needs the interface, and the implementation class just provides the service, so it is the consumer (the client) who defines the interface. With this in mind, you can correctly define a Role interface from the perspective of the consumer, rather than extracting the Header interface from the implementation class.

What is Role interface? For example, bricks can be used by construction workers to build houses, and they can also be used in self-defense:

public class Brick {
    private int length;
    private int width;
    private int height;
    private int weight;

    public void build() {/ /... } public voiddefense() {/ /... Self-defense}}Copy the code

This is called a Header Interface if you simply extract the following Interface:

public interface BrickInterface {
    void buildHouse();
    void defense();
}
Copy the code

The general public needs weapons to defend themselves, not houses of brick. Interface isolation is violated when a Person is forced to rely on interface methods that he or she does not need. The correct approach is to abstract the Role interface from the perspective of consumers:

public interface BuildHouse {
    void build();
}

public interface StrickCompetence {
    void defense();
}

public class Brick implement BuildHouse, StrickCompetence {
}
Copy the code

With Role Interfaces, the general public as consumers and construction workers can consume their own interfaces respectively:

Worker.java
brick.build();

Person.java
brick.strike();
Copy the code

The interface isolation principle is essentially the embodiment of the single responsibility principle, and it also serves the Ricci substitution principle. The dependency inversion principle introduced next can be used to guide the implementation of interface isolation principle.

Dependence Inversion Principle

The dependency inversion principle states that high-level modules should not depend on low-level modules, but both should depend on their abstractions.

This principle is a guide to implementing the interface isolation principle. As mentioned earlier, the high-level consumer should not depend on the implementation, but should be defined by the consumer and depend on the Role Interface. The low-level implementation also depends on the Role Interface because it implements the interface.

Dependency inversion is the dividing line between procedural programming and object-oriented programming. The dependence of procedural programming without inversion, A Simple DIP Example | Agile all, Patterns, and Practices in c # Example of this article is based on switch and lamp illustrates this well.

In the diagram above, the Button is dependent on the light when it directly calls the light on and off. The code is entirely procedural:

public class Button {   
    private Lamp lamp;   
    public void Poll()   {
        if(/*some condition*/) lamp.TurnOn(); }}Copy the code

If Button wants to control the TV, what about the microwave? The way to deal with this change is to abstract the Role Interface ButtonServer:

No matter the lamp, or the TV, as long as the implementation of ButtonServer, Button can control. This is object-oriented programming.

conclusion

Overall, applying one of SOLID’s principles alone does not maximize revenue. It should be understood and applied as a whole to better guide your software design. Single responsibility is the foundation of all design principles, open and close principle is the ultimate goal of design. The Richter substitution principle emphasizes the correctness of the program runtime after a subclass replaces its parent class. It is used to help implement the open close principle. The interface isolation principle is used to help implement the Richter substitution principle, and it also embodies a single responsibility. Dependency inversion is the principle that separates procedural programming from OO programming, and it is also used to guide the principle of interface isolation.