[The Beauty of Design Patterns Design Principles and Thoughts: Specification and Refactoring] 30 | Theory 4: How to decouple code through encapsulation, abstraction, modularization, middle layers, etc.?

As we mentioned earlier, refactoring can be divided into large-scale high-level refactoring (referred to as "large refactoring") and small-scale low-level refactoring (referred to as "small refactoring"). Large-scale refactoring is the refactoring of top-level code designs such as systems, modules, code structures, and relationships between classes. 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?
    Without further ado, let's officially start today's study now!

Why is "decoupling" so important?

One of the most important tasks of software design and development is dealing with complexity. Humans are limited in their ability to deal with complexity. 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, and I personally think that the most important thing is decoupling to ensure loose coupling and high cohesion of the code. If refactoring is an effective means to ensure that the code quality will not be corrupted to the point of hopelessness, then using decoupling to refactor the code is an effective means to ensure that the code will not be too complicated to be uncontrollable.

We introduced in Lecture 22 what is "high cohesion and loose coupling". If you are not impressed, you can review it again. In fact, "high cohesion and loose coupling" is a relatively general design idea, which can not only guide the design of fine-grained classes and the relationship 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 code at a higher level.

Whether it is reading code or modifying code, the characteristics of "high cohesion and loose coupling" allow us to focus on a certain module or class without knowing too much about the code of other modules or classes, so that our focus will not be too divergent , reducing the difficulty of reading and modifying the code. Moreover, because the dependencies are simple and the coupling is small, modifying the code will not affect the whole body. The code modification is relatively concentrated, and the risk of introducing bugs is greatly reduced. At the same time, "high cohesion, loose coupling" code is also more testable, and it is easy to mock or rarely needs to mock externally dependent modules or classes.

In addition, the code is "high cohesion and loose coupling", which means that the code structure is clear, the layering and modularization are reasonable, the dependencies are simple, and the coupling between modules or classes is small, so the overall quality of the code is Not bad. Even if a specific class or module is not well designed and the code quality is not very high, the scope of influence is very limited. We can focus on this module or class and do small refactorings accordingly. Compared with the adjustment of the code structure, the difficulty of such small-scale 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 conforms to "high cohesion and loose coupling"? In other words, how to judge whether the system needs to be decoupled and refactored?

There are many indirect measurement standards, some of which we mentioned earlier, for example, to see if modifying the code will affect the whole body. In addition, there is another direct measure, which I often use when reading the source code, that is to draw the dependencies between modules and between classes, according to the dependency diagram The complexity to determine whether decoupling refactoring is required.

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

How to "decouple" the code?

Earlier we were able to explain the importance of decoupling and how to judge whether decoupling is needed. Next, let’s take a look at how to do decoupling.

1. Encapsulation and abstraction

Encapsulation and abstraction, as two very general design ideas, 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 for us 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 abstraction rather than specific implementation, when we change the underlying implementation of the open() function, we do not need to change the upper-level code that depends on it, which is also in line with what we mentioned earlier The criteria for judging "high cohesion, loose coupling" code.

2. Middle layer

Introducing an intermediate layer simplifies dependencies between modules or classes. The picture below is a comparison of dependencies before and after the introduction of the middle layer. Before introducing the middle layer of data storage, the three modules A, B, and C all depend on the three modules of memory level-1 cache, Redis level-2 cache, and DB persistent storage. After the middle layer is introduced, 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 significantly simplifies the dependencies and makes the code structure clearer.

In addition, when we are refactoring, the introduction of the middle 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 the design of an interface, we need to modify its definition, and at the same time, all codes that call this interface must be changed accordingly. If the newly developed code also uses this interface, then the development conflicts with the refactoring. In order to allow refactoring to 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 a new interface definition.
  • 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 development workload of each stage will not be very large, and can be completed in a short period of time. The probability of refactoring and development conflicts is also reduced.

3. Modularity

Modularity is a common means of building complex systems. Not only in the software industry, but also in construction, machinery manufacturing and other industries, this method is also very useful. For a large complex system, no one person can control all the details. The main reason why we can build such a complex system and maintain it is to divide the system into independent modules and let different people be responsible for different modules, so that even without knowing all the details, Managers can also coordinate various modules to make the entire system work efficiently.

Focusing on software development, the reason why many large-scale software (such as Windows) can achieve orderly collaborative development by hundreds or thousands of people is also due to the good modularity. Different modules communicate through APIs, and the coupling between each module is very small. Each small team focuses on an independent high-cohesion module for development, and finally assembles each module like building blocks to build a A super complex system.

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

In fact, from the explanation just now, we can also find that the idea of ​​modularization is ubiquitous, such as SOA, microservices, lib library, module division in the system, and even the design of classes and functions, all embody the idea of ​​modularization. If you go back to the source, the more essential thing of modular thinking is to 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 are aimed at achieving "high cohesion and loose coupling" of the code. Let's summarize and review the principles together.

  • Single Responsibility Principle

We mentioned earlier that cohesion and coupling are not independent. High cohesion makes the code more loosely coupled, and an important guiding principle to achieve high cohesion is the single responsibility principle. If the responsibility of a module or class is designed to be single, rather than large and comprehensive, then there will be fewer classes that depend on it and the classes it depends on, and the code coupling will be reduced accordingly.

  • Programming based on interface instead of implementation

Interface-based rather than implementation-based programming can isolate changes from specific implementations through an intermediary layer such as interfaces. The advantage of this is that between two modules or classes that have dependencies, changes in 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 rather than implementation, dependency injection also changes the strong coupling between codes into weak coupling. Although dependency injection cannot decouple two classes that should have dependencies into no dependencies, it can make the coupling relationship less tight and make it easy to plug and replace.

  • Use more composition and less inheritance

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

  • Law of Demeter

Dimit's law 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 the 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, so 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. For this part, we will leave it in the design pattern module to explain slowly.

key review

Well, that's all for today's content. Let's summarize and review the content you need to master.

1. Why is "decoupling" so important?

Overly complex code is often unfriendly in terms of readability and maintainability. Decoupling ensures loose coupling and high cohesion of code, which is an effective means to control code complexity. The code is highly cohesive and loosely coupled, which means that the code structure is clear, the hierarchical modularization is reasonable, the dependencies are simple, and the coupling between modules or classes is small, so the overall quality of the code will not be bad.

2. Does the code need to be "decoupled"?

There are many indirect measurement criteria, for example, whether modifying the code affects the whole body. The direct measurement standard is to draw the dependencies between modules and modules, classes and classes, and judge whether decoupling refactoring is needed according to the complexity of the dependency graph.

3. How to "decouple" the code?

The methods for code decoupling include: encapsulation and abstraction, middle layer, modularization, and some other design ideas and principles, such as: single responsibility principle, programming based on interface rather than implementation, dependency injection, multi-use combination and less use of inheritance, Di Mitter's law, etc. Of course, there are some design patterns, such as the observer pattern.

class disscussion

In fact, in our usual development, the idea of ​​decoupling can be seen everywhere. For example, AOP in Spring can realize the decoupling of business and non-business code, and IOC can realize the decoupling of object construction and use. In addition, what other decoupling application scenarios can you think of?

Welcome to write down your thoughts and answers in the message area, and communicate and share with your classmates. If you gain something, you are welcome to share this article with your friends.

Guess you like

Origin blog.csdn.net/qq_32907491/article/details/129870243