Geek Time-The Beauty of Design Patterns Theory 12: How to decouple code through encapsulation, abstraction, modularization, and middle layers?

. For large-scale refactoring, today we will focus on the most effective method, which is "decoupling." The purpose of decoupling is to achieve high code cohesion and loose coupling. Regarding decoupling, I am going to explain it to you in the following three parts.

● Why is "decoupling" so important?

● How to determine whether the code needs to be "decoupled"?

● How to "decouple" the code?

Why is "decoupling" so important?

One of the most important tasks in software design and development is to deal with complexity. Human ability to deal with complexity is limited. Overly complex code is often unfriendly in terms of readability and maintainability. So how to control the complexity of the code? There are many methods. I personally think that the most important thing is decoupling to ensure loose code coupling and high cohesion. If refactoring is an effective means to ensure that the quality of the code is not corrupted to the point of hopelessness, then the use of decoupling methods to refactor the code is an effective means to ensure that the code is not too complex to be uncontrollable.

In fact, "high cohesion and loose coupling" is a more general design idea, which can not only guide the design of fine-grained classes and relationships between classes, but also guide the design of coarse-grained systems, architectures, and modules. Compared with coding standards, it can improve the readability and maintainability of the code at a higher level.

Whether reading the code or modifying the code, the "high cohesion and loose coupling" feature allows us to focus on a certain module or class, without having to know too much about the code of other modules or classes, so that our focus will not be too divergent , Which reduces the difficulty of reading and modifying the code. Moreover, because the dependency is simple and the coupling is small, modifying the code will not affect the whole body. The code modification is more concentrated, and the risk of introducing bugs is reduced a lot. At the same time, the "high cohesion and loose coupling" code is more testable, easy to mock or rarely need to mock externally dependent modules or classes.

In addition, the code is "highly cohesive and loosely coupled", which means that the code structure is clear, the layering and modularization are reasonable, the dependency relationship is simple, and the coupling between modules or classes is small, then the overall quality of the code Not bad. Even if a specific class or module is not designed reasonably well, the code quality is not very high, and the scope of influence is very limited. We can focus on this module or class and do the corresponding small refactoring. Compared with the adjustment of the code structure, this kind of small refactoring with a relatively concentrated range of changes is much easier.

Does the code need to be "decoupled"?

Now the question is, how do we judge the degree of coupling of the code? In other words, how to judge whether the code meets "high cohesion and loose coupling"? Or in other words, how to judge whether the system needs decoupling and reconstruction?

There are many indirect measurement standards, and we have talked about some of them earlier, for example, whether the modification of the code will affect the whole body. In addition, there is a direct measurement standard, which I often use when reading the source code, which is to draw the dependencies between modules and between classes and between classes, according to the dependency graph To determine whether decoupling reconstruction is needed.

If the dependencies are complex and confusing, then in terms of code structure, the readability and maintainability are certainly not very good, then we need to consider whether we can make the dependencies clear and simple through decoupling methods. Of course, this judgment still has a relatively strong subjective color, but it can be used as a means of reference and sorting out dependence, and used in conjunction with indirect measurement standards.

How to "decouple" the code?

Earlier we can talk about the importance of decoupling and how to determine whether decoupling is needed. Next, let's take a look at how to decouple.

1. Encapsulation and Abstraction

As two very general design ideas, encapsulation and abstraction can be applied in many design scenarios, such as the design of systems, modules, libs, components, interfaces, classes, etc. Encapsulation and abstraction can effectively hide the complexity of implementation, isolate the variability of implementation, and provide stable and easy-to-use abstract interfaces for dependent modules.

For example, the open() file operation function provided by the Unix system is very simple to use, but the underlying implementation is very complicated, involving permission control, concurrency control, physical storage, and so on. By encapsulating it into an abstract open() function, we can effectively control the spread of code complexity and encapsulate the complexity in local code. In addition, because the open() function is defined based on an abstract rather than a specific implementation, when we change the underlying implementation of the open() function, we don’t need to change the upper-level code that depends on it, which is also in line with what we mentioned earlier. Criteria for the "high cohesion and loose coupling" code.

2. The middle layer

The introduction of an intermediate layer can simplify the dependencies between modules or classes. The following picture is a comparison of dependencies before and after the introduction of the middle layer. Before the introduction of the data storage middle layer, the three modules of A, B, and C all depend on the three modules of memory first-level cache, Redis second-level cache, and DB persistent storage. After the introduction of the middle layer, the three modules only need to rely on one module for data storage. As can be seen from the figure, the introduction of the middle layer has significantly simplified the dependencies and made the code structure clearer.
Insert picture description here
In addition, when we are refactoring, the introduction of an intermediate layer can play a transitional role, allowing development and refactoring to proceed simultaneously without interfering with each other. For example, if there is a problem with an interface design, we need to modify its definition. At the same time, all the code that calls this interface must be changed accordingly. If the newly developed code also uses this interface, then development conflicts with refactoring. In order to make the refactoring run in small steps, we can complete the interface modification in the following four stages.

● The first stage: Introduce an intermediate layer, wrap the old interface, and provide new interface definitions.

● The second stage: The newly developed code depends on the new interface provided by the middle layer.

● The third stage: Change the code that relies on the old interface to call the new interface.

● The fourth stage: After ensuring that all codes call the new interface, delete the old interface.

In this way, the amount of development work at each stage will not be large, and it can be completed in a short period of time. The probability of conflict between refactoring and development is also reduced.

3. Modularity

Modularization is a common method for constructing complex systems. This method is also very useful not only in the software industry, but also in industries such as construction and machinery manufacturing. For a large complex system, no one can control all the details. The main reason why we can build such a complex system and maintain it is that the system is divided into independent modules, and different people are responsible for different modules, so that even without knowing all the details, Managers can also coordinate various modules to make the entire system operate effectively.

Focusing on software development, the reason why many large-scale software (such as Windows) can be developed methodically by hundreds or thousands of people is also due to the good modularity. Different modules communicate through APIs. The coupling between each module is very small. Each small team focuses on an independent high-cohesion module to develop. Finally, the modules are assembled like building blocks to build A super complex system.

Let's focus on the code level. Reasonable division of modules can effectively decouple the code and improve the readability and maintainability of the code. Therefore, when we develop the code, we must have a sense of modularity. We will develop each module as an independent lib, and only provide the interface that encapsulates the internal implementation details for other modules, which can reduce the number of different modules. The degree of coupling between.

In fact, from the explanation just now, we can also find that the idea of ​​modularity is everywhere, such as SOA, microservices, lib libraries, the division of modules in the system, and even the design of classes and functions, all of which reflect the idea of ​​modularity. If we go back to the source, the more essential thing of modular thinking is divide and conquer.

4. Other design ideas and principles

"High cohesion and loose coupling" is a very important design idea, which can effectively improve the readability and maintainability of the code, and reduce the scope of code changes caused by functional changes. In fact, in the previous chapters, we have mentioned this design idea many times. Many design principles aim at achieving "high cohesion and loose coupling" of code. Let's summarize and review the principles.

Single responsibility principle

As we mentioned earlier, cohesion and coupling are not independent. High internal gatherings make the code more loosely coupled, and the important guiding principle for achieving high cohesion is the single responsibility principle. The responsibilities of a module or class are designed to be single, rather than large and comprehensive. There will be fewer dependent classes and fewer dependent classes, and the code coupling will be reduced accordingly.

Programming based on interface rather than implementation

Programming based on interfaces rather than implementation can isolate changes and specific implementations through an intermediate layer such as interfaces. The advantage of this is that between two modules or classes that have a dependency relationship, changes to one module or class will not affect the other module or class. In fact, this is equivalent to decoupling a strong dependency (strong coupling) into a weak dependency (weak coupling).

Dependency injection

Similar to the idea of ​​programming based on interfaces instead of implementing, dependency injection also turns strong coupling between codes into weak coupling. Although dependency injection cannot decouple two classes that should have a dependency relationship into no dependency, it can make the coupling relationship less tight and easy to plug and replace.

Use more combination and use less inheritance

We know that inheritance is a strong dependency relationship. The parent class and the child class are highly coupled, and this coupling relationship is very fragile. It affects the whole body. Every change of the parent class will affect all the subclasses. On the contrary, the composition relationship is a weak dependency relationship, which is more flexible. Therefore, for code with a more complex inheritance structure, using composition to replace inheritance is also an effective means of decoupling.

Dimit's Law

Dimit's rule says that there should be no dependencies between classes that should not have direct dependencies; between classes that have dependencies, try to only rely on necessary interfaces. From the definition, we can clearly see that the purpose of this principle is to achieve loose coupling of code. As for how to apply this principle to decouple code, you can go back and read Lecture 22, I won't go into details here.

In addition to the design ideas and principles mentioned above, there are also some design patterns for decoupling dependencies, such as the observer pattern. We will stay in the design pattern module and explain it slowly.

Guess you like

Origin blog.csdn.net/zhujiangtaotaise/article/details/110442338