Abstract: There is a saying very well, "Code quality determines the quality of life". When you reduce the complexity of the software, reduce the number of bugs, and increase the maintainability of the system, it naturally brings a better quality of life. .
This article is shared from the Huawei Cloud Community "The complexity of the code written is too high? at what the experts say" 160a4b0716a263, original author: Yuan Runzi.
Preface
When developing software, we often pursue high maintainability of the software. High maintainability means that when new requirements come, the system is easy to expand; when there are bugs, developers are easy to locate. When we say that the maintainability of a system is too poor, it often means that the system is too complicated, which leads to bugs when adding new functions to the system, and it is difficult to locate after bugs.
So, how is the complexity of software defined?
The definition given by John Ousterhout is as follows:
Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.
It can be seen that the complexity of software is a very broad concept. Anything that makes the software difficult to understand and difficult to modify belongs to the complexity of the software. To this end, John Ousterhout proposed a formula to measure the complexity of a system:
In the formula, pp represents the module in the system, c_{p}cp represents the cognitive load of the module (Cognitive Load, that is, the degree to which a module is difficult to understand), and t_{p}tp represents the module in the daily development Development time spent.
From the formula point of view, the complexity of a software is accumulated by the complexity of its various modules, and module complexity = module cognitive burden * module development time, that is, the complexity of the module is related to the module itself, but also to The development time spent on this module is related. It should be noted that if a module is very difficult to understand, but it is hardly involved in the subsequent development process, then its complexity is also very low.
Causes of software complexity
The reasons for the complexity of the software can be broken down into many categories, but they can be summed up in two categories: dependencies and obscurity. The former will make it difficult to modify and bugs are easy to occur. For example, when modifying module 1, it often involves the changes of module 2, module 3, ...; the latter will make the software difficult to understand, locate a bug, or even just It takes a lot of time to read a piece of code.
The complexity of software is often accompanied by the following symptoms:
Shotgun Modification (Change amplification) . When only one function needs to be modified, but many modules have to be modified, we call it a shotgun modification. This is usually caused by excessive coupling between modules and too much interdependence. For example, there is a set of Web pages, each page is an HTML file, and each HTML has a background attribute. Since the background attributes of each HTML are defined separately, if you need to change the background color from orange to blue, you need to modify all HTML files.
Cognitive load . When we say that a module is obscure and difficult to understand, it has an excessive cognitive burden. In this case, it often requires readers to spend a lot of time to understand the function of the module. For example, to provide a calculate interface without any annotations, it has 2 input parameters of type int and a return value of type int. Judging from the signature of the function, the caller has no way of knowing the function of the function. He can only call the function after he spends time reading the source code to determine the function of the function.
int calculate(int val1, int val2);
Unknown unknowns . Compared with the first two symptoms, uncertainty is more destructive. It usually refers to some points that you must pay attention to when developing requirements, but you cannot know. It is often caused by some obscure dependencies, which will make you feel very distracted after developing a requirement, and vaguely feel that there is a problem with your code, but you don’t know where the problem is. You can only pray that it will be exposed during the test Don't make loopholes in the commercial stage.
How to reduce the complexity of software
Say No to "Tactical Programming"!
When developing features or fixing bugs, many programmers often focus on how to make the program run easily and quickly. This is a typical tactical programming method, which pursues short-term benefits—saving development time. The most common manifestation of tactical programming is that there is no module design before coding, and you can write wherever you think. Tactical programming may be more convenient in the early stage of the system. Once the system becomes large and the coupling between modules becomes heavier, it will become difficult to add or modify functions and fix bugs. As the system becomes more and more complex, finally the system has to be refactored or even rewritten.
The opposite of tactical programming is strategic programming, which pursues long-term benefits—increasing system maintainability. It is not enough just to make the program run. It is also necessary to consider the maintainability of the program so that it can respond quickly when adding or modifying functions and fixing bugs in the future. Because there are many points to consider, it is destined to spend a certain amount of time for module design in strategic programming, but compared to the problems caused by tactical programming in the later stage, this time is completely worthwhile.
makes the module "deeper"!
A module consists of two parts: interface and implementation. If you compare a module to a rectangle, then the interface is the edge of the top of the rectangle, and the realization is the area of the rectangle (you can also think of the implementation as provided by the module Features). When the function provided by a module is constant, the feature of the deep module is that the sides of the top of the rectangle are relatively short, and the overall shape is tall and thin, that is, the interface is relatively simple; the feature of the shallow module is that the sides of the top of the rectangle are relatively short. It is long, and the overall shape is short and fat, that is, the interface is more complicated.
Users of a module often only see the interface. The deeper the module, the less information the module exposes to the caller, and the lower the coupling between the caller and the module. Therefore, the "deeper" module is designed to help reduce the complexity of the system.
So, how can we design a deep module?
- Simpler interface
A simple interface is more important than a simple implementation. A simpler interface means that the module is easier to use and the caller is more convenient to use. The simple implementation + complex interface form, on the one hand, affects the ease of use of the interface, on the other hand, it deepens the coupling between the caller and the module. Therefore, when designing a module, it is best to abide by the principle of "Leave the simplicity to others, and leave the complexity to yourself".
Exceptions are also part of the interface. In the coding process, it should be avoided that exceptions are thrown up at will without processing, which will only increase the complexity of the system.
- More general interface
When designing an interface, you often have two choices: (1) Design as a dedicated interface; (2) Design as a general interface. The former is more convenient to implement and can fully meet current needs, but has low scalability and belongs to tactical programming; the latter requires time to abstract the system, but has high scalability and belongs to strategic programming. A universal interface means that the interface is applicable to more than one scenario, typically in the form of "one interface, multiple implementations".
Some programmers may refute that in the case of unpredictable future changes, general-purpose means over-design. Over-generalization is indeed over-design, but the appropriate abstraction of the interface is not. On the contrary, it can make the system more layered and maintainable.
- Hide details
When designing a module, you must also learn to distinguish which information is important and which information is not important to the caller. Hiding details refers to only exposing important information to the caller and hiding unimportant details. Hiding the details first makes the module interface simpler, and the other makes the system easier to maintain.
How to judge whether the details are important to the caller? Here are a few examples:
1. For the Map interface of Java, important details: each element in the Map is composed of <Key, Value>; unimportant details: how to store these elements at the bottom of the Map, how to achieve thread safety, etc.
2. For the read function in the file system, important details: from which file and how many bytes are read for each read operation; unimportant details: how to switch to kernel mode, how to read data from the hard disk, etc.
3. For multi-threaded applications, important details: how to create a thread; unimportant details: how the multi-core CPU schedules the thread.
for layered design!
A well-designed software architecture has a characteristic, that is, the layers are clear, each layer provides different abstractions, and the dependencies between each layer are clear. Whether it is the classic three-tier Web architecture, the four-tier architecture and hexagonal architecture advocated by DDD, or the so-called Clean Architecture, there is a distinct sense of hierarchy.
In the layered design, it should be noted that each layer should provide different abstractions, and try to avoid a large number of Pass-Through Mehod in a module. For example, in the four-tier architecture of DDD, the domain layer provides abstraction of domain business logic, the application layer provides abstraction of system use cases, the interface layer provides abstraction of system access interfaces, and the infrastructure layer provides access to databases such as This kind of abstraction of basic services.
The so-called Pass-Through Mehod refers to those functions that "call other functions directly in the body of the function, but do very little by themselves". Usually, their function signatures are very similar to the function signatures that are called. The module where Pass-Through Mehod is located is usually a shallow module, which adds unnecessary levels and function calls to the system, which makes the system more complicated:
public class TextDocument ... {
private TextArea textArea;
private TextDocumentListener listener;
...
public Character getLastTypedCharacter() {
return textArea.getLastTypedCharacter();
}
public int getCursorOffset() {
return textArea.getCursorOffset();
}
public void insertString(String textToInsert, int offset) {
textArea.insertString(textToInsert, offset);
}
...
}
Learn to write code comments!
Annotation is an extremely cost-effective method in the software development process. It only takes 20% of the time to obtain 80% of the value. It can improve the readability of obscure code; it can hide the complex details of the code. For example, interface comments can help developers quickly understand the function and usage of the interface without reading the code; if well written , It can also improve the design of the system.
Specifically how to write code comments, refer to "How to write excellent code comments? " .
to sum up
Software complexity is something we programmers must face in daily development. Learn how to "figure out what is software complexity, find out the reasons that lead to software complexity, and use various methods to overcome software complexity" A necessary ability. There is a saying that “code quality determines the quality of life”. When you reduce the complexity of the software, reduce the number of bugs, and increase the maintainability of the system, you naturally bring a better quality of life.
Modular design is the most effective means to reduce the complexity of software. Learn to use the method of "strategic programming" and stick to it. We often advocate "doing things right once", but this is not applicable to module design. Almost no one can design a module to be perfect the first time. Secondary design is a very effective technique. Instead of spending a lot of time on refactoring or rewriting after the system is corrupted, it is better to spend some time on secondary design after completing the module design for the first time. Ask yourself more : Is there a simpler interface? Is there a more general design? Is there a more concise and efficient implementation?
"Rome was not built in a day", the same is true for reducing the complexity of the software.
Click to follow to get to know the fresh technology of Huawei Cloud for the first time~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。