Does high-quality code depend on design or refactoring?

Click on the link for details

img


guide

A programmer who pursues must hope that he can write high-quality code, but where does high-quality code come from? Some people think that it is designed, just like a solid building. If there is no excellent design in the early stage, it will definitely not escape the fate of a tofu project; Software decays over time, so constant refactoring is required to maintain the high quality of the code. Which statement is more reasonable? Today I will talk to you about the relationship between refactoring, design and high-quality code. Welcome to read.

Table of contents

1 Start with a case

1.1 Chaos

1.2 Order

2 Code Quality

2.1 Write understandable (readable) code

2.2 Avoid surprises

2.3 Coding hard-to-misuse code

2.4 Realize code modularization

2.5 Write reusable and scalable code

2.6 Write testable code and test it appropriately

2.7 Summary

3 Programming Paradigms, Design Principles and Design Patterns

3.1 Programming paradigm

3.2 Design principles

3.3 Design Patterns

4 Technical debt, code smells, and refactoring

4.1 Technical Debt

4.2 Bad code smells

4.3 Refactoring

5 summary

01

from a case

The story has to start with a performance optimization experience. In the information flow business, it is often necessary to display pictures. If the size of the picture is too large, it will cause performance problems. Image size. Thanks to Tencent's powerful infrastructure, the business side does not need to process the pictures themselves, but only needs to modify the parameters of the picture link to adjust the cropping and compression size of the picture.

img

What you need to know here is that I wrote an image optimization tool to modify the image link representing the original image into a suitable image link. For example, the above link in the picture below is the original picture link, the so-called original picture is the unprocessed picture uploaded by the user. After the original image is processed, the following new image link is obtained. The difference between the new link and the original image link is that two parameters are added, which respectively represent the cropping ratio and the compressed size.

img

The next question is how to write the code of this "picture optimization tool".

1.1 Chaos

img

To sort out the business logic, the steps to link the original image to the best link can be divided into 3 steps:

Determine whether the original image link complies with the cropping rules, and if so, calculate the optimal cropping ratio. You can’t just give https://qq.com to crop directly, you need to verify the link; judge whether the link of the original image conforms to the compression rules, and if so, calculate the optimal compression rule. The reason is similar to the above, some pictures do not support compression, so it also needs to be judged; stitching new picture links.

Now that the logic is clear, the code is not difficult to write. The pseudo-code as far as the eye can see is full of if-else conditional branches, and a seemingly simple piece of code is actually twisted and twisted.

if (必要参数是否填写) {if (指定域名 && 符合裁剪规则) { // 计算最优裁剪比例 }if (指定域名 && 符合压缩规则) { // 计算最优压缩规则 }if (指定域名) { // 计算新图链接 }}

This is not over yet, suddenly the product manager said: "Support two more new domain names". Due to some historical reasons, there are many similar services in the company, but the capabilities are similar, and they all modify the cropping and compression size of the image by changing the parameters in the link. For example, as shown in the figure below, some domain names support clipping and compression at the same time, but the path needs to be modified directly; some domain names do not support clipping, and parameters are also passed in through query. Therefore, we need to make different judgments for different domain names.

img

It seems to have become a common practice for soldiers to block water and soil, and to change the code when a new requirement comes. In this business scenario, use if else to judge different domain names, and then deal with them separately.

if (A 域名) {// 计算 A 域名最佳链接} else if (B 域名) {// 计算 B 域名最佳链接} else if (...) {  ...}

Here I have to mention an indicator closely related to software quality: cyclomatic complexity. According to Wikipedia's definition, "Cyclomatic complexity is a software measure used to represent the complexity of a program. Cyclomatic complexity is measured by the number of linearly independent paths in the program's source code." The logic of processing a single domain name has exceeded 10 cyclomatic complexity. Every time a new domain name is added, the cyclomatic complexity will increase linearly by 10. It is conceivable that the complexity of this logic can be increased by adding a few new domain names. It's too high to comprehend. Therefore, it is imminent to refactor this code.

1.2 Order

Most of the power of programs comes from conditional logic, but unfortunately, the complexity of programs often comes from conditional logic. As can be seen from the example just now, if not controlled, our code will soon be full of if else, so we need to simplify the conditional logic. There are six suggestions in the book Refactoring.

Decompose Conditional Expressions Merge Conditional Expressions Replace Nested Conditional Expressions with Guard Statements Replace Conditional Expressions with Polymorphism Introduce Special Cases Introduce Assertions

Let's see what suggestions can come in handy.

1.2.1 Refactoring method 1: Decomposing conditional expressions

The so-called decomposing conditional expression is to decompose a huge logical code block into independent functions. The name of each function can express its intention. Just like the modification in the example, the code can highlight the conditional logic. Each branch effect is also clearer.

img

1.2.2 Refactoring method 2: Replacing conditional expressions with polymorphism

Is it enough to just decompose the conditional expression? It is definitely not enough. Because the structure of such code is not clear enough, in fact, we can split different conditional logic into different scenarios, and use polymorphism to split the logic more clearly.

img

img

First declare a Strategy base class, pass in the necessary parameters in the constructor, and provide a getter function for calculating the best picture link. Whenever a new field needs to be processed, declare a class that extends the Strategy base class. In this way, the code using polymorphism is logically clearer.

1.2.3 Refactoring method 3: Strategy pattern

The book "Design Patterns" mentions: "Composition is better than inheritance". If we analyze the above polymorphic code in detail, we will find that there is no natural inheritance relationship between AdomainStrategy and Stragtegy, and inheritance will increase system coupling, while combination can better achieve runtime flexibility. Then we can further refactor this code using the strategy pattern.

The so-called "strategy pattern" is "to define a series of algorithms, encapsulate them one by one, and make them interchangeable".

img

img

Each specific strategy class passes in the required parameters through the constructor and provides a public method named getImgUrl. In the strategy context class, set specific strategies and provide methods to obtain the optimal image address.

1.2.4 Refactoring Method 4: Template Method Pattern

After further analysis, it is found that whether it is a domain name or B domain name, or any other domain name, the processing logic has the same part, that is, "calculate cropping ratio" -> "calculate compression specification" -> "splicing new links" -> "return new the link to".

img

When writing high-quality code, you should be wary of repeated code, and this is no exception. We can use the "template method pattern" to further refactor. The so-called "template method pattern" means "in the template method pattern, the same subclass implementation Parts are moved up into the parent class, leaving the different parts to be implemented by subclasses". To implement it in the code is to declare an abstract class, extract the public logic into the abstract class, and then implement the specific business logic in the subclass.

img

1.2.5 Summary

This is a piece of logic that is not complicated, but if you leave it alone, the code will become more and more complicated until it cannot be maintained. Therefore, we have carried out multiple rounds of refactoring on this code, starting from decomposing the conditional expression at the beginning, to adopting polymorphism to make the code structure clearer, and further adopting the strategy mode and template method mode to continuously improve the code quality.

Now if the product manager says to support two new domain names again, it will be very simple. You only need to inherit ImageStrategy, and then implement the three business-related methods in turn.

img

02

code quality

Earlier we spent a lot of time introducing how image optimization tools are refactored step by step. You may have some feelings about high-quality code, but what is code quality? What are we talking about when we talk about code quality? ? Below we will make this concept more clear.

The book Good Code, Bad Code proposes that code should be written to meet 4 high-level goals:

The code should work: It goes without saying that if the code you write doesn't work, it's not good quality code. But it needs to be mentioned that the premise of "normal work" mentioned here is that we should correctly understand the requirements. If the requirements have high requirements for performance or security, then these requirements should be included in the category of "normal work"; Code should continue to work: It might work fine when it first goes live, but have you ever woken up and suddenly stopped working? This is because our code is not isolated from the world, but has various dependencies and interactions with the surroundings. For example, our code may depend on other codes, and those codes may be abnormal; the code should adapt to changing needs: Software is called software precisely because of its easy-to-change nature. In the face of changes that may or may not occur in the future, we can deal with them in two ways. One is to try to predict all possible changes, and then leave a lot of expansion points; the other is to completely ignore future changes. Obviously these are two extremes, and we should seek a balance between the two; the code should not repeat the work done by others: this goal can be understood from two directions, if other engineers have ready-made solutions, we should reuse them ; and if we write a solution ourselves, it should be structured in a better form so that other engineers can easily reuse it.

Although we know the goal of high-quality code, we can't do it just by knowing the goal, and we need more specific strategies. Why many people hate chicken soup is because those people only give chicken soup but not a spoon. We give both goals and specific strategies here. These are the six pillars of code quality. Next, I will introduce the six pillars of code quality in combination with the examples mentioned above. It should be noted that each pillar represents a principle, and there can be many specific measures to realize this principle.

img

2.1 Write understandable (readable) code

Writing easy-to-understand code is the first pillar of the six pillars of code quality. Let's take 3 different examples to see how to implement the idea of ​​this pillar.

2.1.1 Use descriptive names

In the previous policy context class, the method name of "get the best picture URL" is getOptimalImgUrl, and its meaning can be seen at a glance from this name. But if you change it to getU or getUrl or calcU, I believe you will be stunned for a few seconds or even half a day and may not be able to guess what it means, then such code is obviously not easy to understand.

2.1.2 Appropriate use of annotations

Comments can often help us understand code better, but aren't all comments good? We can look at the comments of the following code, which contains two comments, one is to describe the purpose of the method, and the other is to describe the specific implementation. The second comment is a bad comment, because engineers have to maintain this comment when the code is maintained, and if the implementation is updated, they need to remember to change the comment. At the same time, if there are 50 lines of invalid comments like this in 100 lines of code, it will increase the difficulty of reading the code.

img

So how to use annotations correctly? Annotations can be used to illustrate the following:

Explain what the code does; explain why the code does what it does; provide other information, such as how-to instructions.

2.1.3 Adhere to a consistent programming style

img

According to our habits, we may think that ImageStrategyContext is a class, and getOptimalImgUrl is its static method. The reason for such a misunderstanding is that the naming style of the variable is wrongly written.

2.2 Avoid surprises

Code is often built in a multi-layered form. High-level code depends on low-level code. The code we write is often only a small part of a large-scale system. We depend on other people's code, and others may also depend on our code. The way everyone collaborates is the code contract, what is the input and what is the output, but this kind of contract is not stable in many cases, and unexpected situations often occur. And what we need to do is to avoid accidents as much as possible.

2.2.1 Avoid writing misleading functions

"Avoid writing misleading functions" is a way to avoid surprises. For example, in the code in the figure below, the code contract of using kdImageStrategy is that the incoming parameter must be an object, and the object must contain the two parameters imgUrl and imgWidth. However, if you encounter parameters that do not meet the requirements, you should actively throw an error.

img

2.3 Write code that is hard to misuse

As mentioned earlier, a system is often the result of the collaboration of many people. If a piece of code is easily misused, according to Murphy's Law, it will be misused sooner or later and the software will not work properly. Therefore, we should write code that is difficult to misuse.

This is the code in the strategy context from the previous example, which actually fails the "write hard-to-misuse code" pillar because it places no restrictions on who calls setImageStrategy, so that instances of ImageStrategyContext will Be misused.

img

And if the code is changed to only set the value at construction time, then the class programming can be made immutable, which greatly reduces the possibility of the code being misused.

img

2.4 Realize code modularization

One of the tenets of modularity is that we should create code that can be easily tweaked and reconfigured without knowing exactly how to tweak or reconfigure it. The key point to achieve this goal is that different functions should be accurately mapped to different parts of the code base.

2.4.1 Consider using dependency injection

Code often needs to depend on low-level problems when solving high-level problems, but sub-problems do not always have only one solution, so it is helpful to allow sub-problem solutions to be reconfigured when structuring code, and dependency injection can help us accomplish the goal.

img

In the code below, ImageStrategyContext has a hard-coded dependency on its sub-solutions, directly declaring two instances of the strategy internally.

img

We can use dependency injection to pass in the strategy instance through the constructor, which can better achieve code modularization.

img

2.5 Write reusable and scalable code

When writing code, you often need to refer to other people's code. If you need to write everything yourself, it is difficult to achieve large-scale cooperation. The key here is to write reusable and scalable code.

2.5.1 Keeping centrality of function parameters

In the method setOptimalCompressionRatio for calculating the optimal compression ratio in the code below, two parameters, compressionRatio and cropRatio, are passed in, but in fact only compressionRatio needs to be passed in. Having more function parameters than needed can make code difficult to reuse. We can delete the cropRatio parameter and let the function only get the parameters it needs.

img

2.6 Write testable code and test it appropriately

When we modify the code, it is very likely to introduce bugs unintentionally, so we need a means to ensure that the code can continue to work normally, and testing is the main means to provide this guarantee.

2.6.1 Testing one behavior at a time

We should ensure that a use case only tests one behavior, if mixed with many behaviors, it is likely to lead to lower test quality.

img

2.7 Summary

This section mainly introduces the six pillars of code quality through the example of the image optimization tool mentioned above. Each pillar has many specific implementation methods. Because of the relationship of time, 3 pillars are cited in the first pillar. 1 example to illustrate "write understandable (readable) code", while in the remaining pillars, only 1 example is given each.

03

Programming Paradigms, Design Principles, and Design Patterns

I just introduced the four goals and six pillars of high-quality code. You may find that the code adopts an object-oriented style. Does that mean that only object-oriented code can write high-quality code? Let's talk to you about programming paradigms and more specific design principles and design patterns.

3.1 Programming paradigm

The programming paradigm refers to the programming style, which has little to do with the specific programming language. For example, JavaScript is a multi-paradigm language. Even the well-known object-oriented language like Java has added a lot of elements of functional programming.

According to the division of "The Way of Clean Architecture", the mainstream programming paradigm mainly includes three types, structured programming, object-oriented programming and functional programming.

3.1.1 Structured programming

I don't know if you have thought about such a question? Object-oriented programming is called object-oriented because the main design element is the object, while the main design element of functional programming is the function. What about structured programming, is its main design element structure? This doesn't seem quite right either. In fact, the so-called structured is relative to unstructured programming.

If you want to understand structured programming, you must go back to that ancient era and see what unstructured programming is like. The left side of the figure below is a piece of code written in Java, which is a piece of structured object-oriented code. The right side of the screen is the decompiled bytecode of the code on the left. The code starts to execute from line 0, but executes to On line 6, a judgment will be made, and if the result is true, it will jump to line 14, and line 11 is similar. You can see that one of the main features of unstructured programming is the jump of execution logic through Goto.

img

Dijkstra wrote a short and concise paper: "Goto is Harmful", in which he pointed out that the Goto statement will make the program difficult to understand and maintain, and instead, structured programming should be used to provide a clearer flow of control.

Prior to this, someone has proved that any program can be constructed using the three structures of sequence structure, branch structure, and loop structure, and thus structured programming was born. Structured programming restricts the direct control of program execution, and no longer allows Goto to jump at will.

Structured programming should be the programming paradigm we are most familiar with, but we rarely mention structured programming when writing high-quality code. That is because structured programming cannot effectively isolate changes and needs to be used in conjunction with other programming paradigms.

3.1.2 Object-Oriented Programming

Object-oriented programming may be heard by most programmers. When our code size gradually expands, the dependencies of each module in structured programming are very strong, which makes it impossible to effectively isolate, and object-oriented can provide better code organization.

When it comes to object-oriented, you may think of three characteristics of object-oriented: encapsulation, inheritance and polymorphism. Encapsulation is the foundation of object-oriented. If the unit module is well encapsulated, it will be more stable, so that larger modules can be built. Inheritance can generally be looked at from two perspectives: if you look up from the perspective of a subclass, more consideration is given to code reuse, which is called "implementation inheritance"; from another perspective, if you look down from the perspective of a parent class Look, more consideration is polymorphism, which is called "interface inheritance". But it is its third feature that really makes object-oriented work: polymorphism. We can quote a passage from Robert Martin in his "Clean Architecture": "Object-oriented programming is the ability to control dependencies in source code by means of polymorphism. This ability allows software architects to A plug-in architecture can be built to separate high-level strategic components from low-level implementation components, and low-level components can be compiled into plug-ins to achieve development and deployment independent of high-level components.”

3.1.3 Functional programming

Functional programming is a programming paradigm that provides us with functions. However, this function is actually the same as the function f(x) in high school mathematics. Recall the function f(x) in high school. The same input will definitely give the same output. The same is true in functional programming. We Avoid states and side effects as much as possible. The functions we usually hear are first-class citizens, currying, and pipelines are all techniques used in functional programming.

The power of functional programming lies in immutability, because "all competition problems, deadlock problems, and concurrent update problems are caused by mutable variables. If variables will never be changed, it is impossible to have competition or concurrency update problem. If the lock state is immutable, there will never be a deadlock problem".

3.1.4 The nature of programming paradigms

Three mainstream programming paradigms were introduced earlier. But we have thought about why we have to create such paradigms, if else can't do it all at once? Is it driven by KPI, or is it the involution of the industry?

Let's look at a classic book first: "Algorithms + Data Structures = Programs". This book will focus on separating algorithms and data structures. Algorithms are methods and steps to solve problems, and data structures are how to organize and store data. Way.

But what you may not be familiar with is that just 3 years after the publication of this book, another paper was published called: "Algorithm = Logic + Control". This paper believes that "any algorithm will have two parts, one is the Logic part, which is used to solve the actual problem. The other is the Control part, which is used to decide what strategy to use to solve the problem... If Logic and Control parts are effectively separated, then the code becomes easier to improve and maintain".

Take a piece of code to calculate factorial to explain it in detail. The so-called factorial is the product of all positive integers less than or equal to a certain integer. We can use recursion or loop to realize it. No matter which algorithm is used, the same result can be obtained. Therefore, it can be said that recursion or loop is Control, and the calculation rule of factorial is Logic.

img

Through the previous two expressions, we can easily conclude that program = logic + control + data structure, and so many programming paradigms are actually centered around these three things. Therefore, we can find that the essence of the programming paradigm is:

Control can be standardized; control needs to process data, so if you want to standardize control, you need to standardize the data structure; control also needs to process the user's business logic, that is, logic.

Effectively separating logic, control, and data structures is the key to writing high-quality code.

Finally, let’s go back to the example of our image optimization tool. Regardless of whether it is a structured approach or an object-oriented approach, the business logic of filtering out the most suitable image link is the same, but the control part is different, and the corresponding , that is, the data structure also needs to be standardized.

img

3.2 Design principles

Through the programming paradigm, we know the design elements such as objects and functions, and the essence of programming is to separate logic, control and data, so how to do it? Design principles give us some more detailed principles to help us better achieve our design goals.

The SOLID principle was proposed by Robert Martin, and it is elaborated in his "Agile Software Development: Principles, Practices, and Patterns" and "The Way of Clean Architecture". The SOLID principle not only has guiding significance in the object-oriented field, but also gives us the principles that design should follow in the field of software design, and also gives us a ruler to measure the effectiveness of the design.

3.2.1 Single Responsibility Principle

The single responsibility principle seems to be the simplest of the five principles, but it is also the most misunderstood one. Many people mistakenly think that a single responsibility is to do only one thing. Robert Martin defined Single Responsibility in two books written 20 years apart.

In "Agile Software Development", Robert Martin pointed out that single responsibility means that "as far as a class is concerned, there should be only one reason for its change". changes are taken into account.

Twenty years later, Robert Martin defined a single responsibility in "The Way of Clean Architecture", "Any software module should only be responsible for a certain type of actor." This time, he took into account not only the change but also the source of the change.

img

But when one domain name has one calculation rule, we can easily distinguish the difference between the two, so we need to split into two categories. But the actual business is complex and changeable, and different businesses are distinguished in the B domain name, and each business is different, so do the X business and Y business under the B domain name still need to be split? If you don't have a deep understanding of the single responsibility, you may think that there is no need for splitting, but now that we know that the single responsibility needs to consider changes and the source of the change, then we can naturally know that splitting is required.

3.2.2 Open and closed principle

Recalling our daily work, we usually change the code once after each requirement. Although once or twice is harmless, this behavior of continuous code modification often causes great damage to the code, resulting in higher and higher maintenance costs. Since modifying the behavior will cause many problems to the code, can it not be modified? You may think I'm talking nonsense, but the principle of openness and closure provides us with such a direction.

Compared with the past when we implemented new requirements by modifying the code, the principle of openness and closure suggests that we implement new requirements through expansion. If you add a new domain name to the code before refactoring, you need to modify the original code, but in the code that conforms to the open-close principle after refactoring, you only need to add a new class.

img

In fact, the principle of opening and closing is very close to us, and it is with us every day. The plug-in systems we use, such as Chrome plug-ins and VSCode plug-ins, all reflect the principle of opening and closing.

3.2.3 Liskov Substitution Principle

The definition of the Liskov substitution principle is somewhat awkward: "If for every object o1 of type S there exists an object o2 of type T such that the behavior of a program P operating on type T remains the same when o1 is replaced by o2, we S can be called a subtype of T". To put it bluntly, "a subtype must be able to replace its parent type".

The rectangle/square problem is a notorious design violation of the Liskov substitution principle. In middle school textbooks, teachers will tell us that a square is a special rectangle. Therefore, in software design, we will naturally inherit the square from the rectangle. But here comes the problem. To set a rectangle, you need to set the length and width attributes, but you only need to set one for the square. According to the Liskov substitution principle, the square class should be able to replace the rectangular class, then we should be able to set the length and width of the square, but in fact it is not possible, we cannot set the length and width of the square at the same time, so this case does not meet the Liskov substitution in principle.

In the case of the image optimization tool, each specific strategy can freely replace its parent Strategy, so it complies with the Liskov substitution principle.

img

3.2.4 Interface Segregation Principle

The Interface Segregation Principle states that "users should not be forced to depend on methods they don't use". This expression seems easy to understand, especially from the user's point of view, the interfaces I don't need certainly don't need to depend on. But as a module designer, there will be an impulse to design the interface too fat. In the figure below, the OPS class provides methods op1, op2, and op3 for User1, User2, and User3 respectively. Any modification to op1 in OPS may affect User2 or User3.

img

3.2.5 Dependency Inversion Principle

The principle of dependency inversion refers to "high-level modules should not depend on low-level modules, both should depend on abstractions". In other words, "Abstraction should not depend on details, details should depend on abstraction". Specific examples have also been introduced in the previous explanation of dependency injection, so I won't go into details here.

But I want to mention that the principle of dependency inversion is recommended here, not to say that this principle should be regarded as a golden rule, because actual development will inevitably rely on some specific implementations.

img

3.3 Design Patterns

SOLID gave us 5 principles that we need to follow when designing, and we also need specific methods to implement, that is, design patterns.

The so-called model is a solution to some common problems. The concept of model was first proposed in the construction industry, but it is true that it is blooming inside the wall and outside the wall. The model shines in the software field. The design pattern in the software field refers to: "A design pattern is a description of classes and objects that communicate with each other that are used to solve general design problems in specific scenarios."

"Design Patterns" provides 23 design patterns, which are actually the application of SOLID principles in 23 scenarios.

I remember that the teacher told us when I was learning design patterns at school before. Many people may be using design patterns without knowing it. This is possible. The prerequisite is to be familiar with the SOLID principles. , and have not specifically learned design patterns, but it is probably bragging to say that you may be using design patterns.

Here I don't want to introduce design patterns too much, because they are essentially the application of SOLID principles in different scenarios.

04

Technical debt, code smells, and refactoring

Having talked so much about design, does that mean that good code is just designed?

From the actual situation, although we have done a lot of design in the early stage of writing code and left a lot of expansion points, we still find that the code is gradually rotting. How did this situation happen, and how can it be resolved?

4.1 Technical Debt

The so-called technical debt refers to "in order to speed up software development, developers have compromised when they should adopt the best solution, and switched to a solution that can speed up software development in the short term, thus bringing additional development burden to themselves in the future".

This technical choice is like a debt that must be repaid in the future, although it may appear to be beneficial in the present. We know that if we live in debt all the time, we may be able to get by in the early stage, but in the later stage, as the debt snowballs, personal bankruptcy will follow, and life will be difficult. To continue. The same principle of life applies to technical projects. If you owe more and more technical debt, the project will eventually fail.

The author of "Refactoring" Martin Fowler divides technical debt into four quadrants according to the two dimensions of intentional or unintentional and hasty prudence, referred to as "Technical Debt Quadrant (Technical Debt Quadrant)". Prudent debts are carefully considered. They know that they owe debts, but after evaluating the early launch of the project and paying off the debts, they find that the benefits of the former are far greater than the costs of the latter, so they choose to go online first. This kind of technical debt is deliberate and intentional. Sloppy debt may not be unintentional, or it may be intentional, where the project team knows that good design is capable of practicing it, but decides it cannot afford the time it takes to pay off technical debt. But in fact, for most developers who have not studied software engineering or practiced good practices, most of them owe sloppy and unintentional technical debt. Finally, it is deliberate and unintentional. When we are developing projects, we often learn while developing. It may take you a year or even longer to understand the best practices of this system.

Fred Brooks, author of The Mythical Man-Month, suggests maybe spending a year designing a system specifically for learning, then scrapping and rebuilding so that a best-practice system can be designed. This suggestion is unacceptable to the vast majority of people, so we should realize that when we are currently doing what we think is the best practice, we are actually owed a technical debt, which is deliberate and unintentional technical debt.

img

4.2 Bad code smells

So how to identify technical debt, we can identify it through bad code smell.

Kent Beck, one of the initiators of the Agile Manifesto, proposed Code Smell code smell, "In the field of program development, any symptom in the code that may lead to deep-seated problems can be called code smell."

Kent's grandma also had a philosophical saying, "If the diaper stinks, change it". In the same way, if we find a bad smell in the code, we replace it.

In this regard, Martin Fowler enumerated 24 kinds of code smells in the book "Refactoring", so that we have a hand to identify code smells, and we can also identify technical debt.

img

4.3 Refactoring

Knowing the bad smell of the code and identifying the technical debt, it is time to repay the debt. The way to repay the debt here is to refactor.

The definition of refactoring in the book "Refactoring" is: "The so-called refactoring (refactoring) is such a process: on the premise of not changing the external behavior of the code, make changes to the code to improve the internal structure of the program. .Refactoring is a methodical method of program organization that has been honed over time to minimize the probability of introducing errors during the organization process. Essentially, refactoring is to improve the design of the code after it is written. "

Give an example of one of the bad tastes, "mysterious naming".

The left side of the picture below is a code full of bad smells. When you read this code, the first thing is getThem. You must be confused. What are they? Continue reading, what is the parameter list? Looking at the function body again, what is list1? what is item[0]? 4 What is it again? Look, a few lines of code has brought us so much confusion, how to read this code.

If you just change the name, change getThem to getFlaggedCells, get the marked cells, and change list to gameBoard, oh, it’s a chessboard, so don’t read it below, you already know that this code is used to read the chessboard that has been flagged Marked checkerboard.

Through this simple example, I believe you understand the process from identifying bad smells to refactoring.

img

05

Summarize

Going back to the question at the beginning of this sharing: "Is good code designed or evolved?" I believe everyone should have the answer.

First of all, we introduced an example of image optimization tools, from the undesigned code in the most common journal form at the beginning, to the relatively concise code after using some refactoring measures, and finally to the application of the strategy pattern and template method pattern code. Through this example, you have a rough understanding of how to write high-quality code through design and refactoring, but this is not the end, but continue to use this example throughout this article.

It then explains what good code is and what we are talking about when we talk about code quality. Borrowing from "Good Code, Bad Code", good code should be able to achieve 4 high-level goals, corresponding to 6 principles, and we keep going back to our original example to illustrate how it meets these 6 principles .

In the example, we focus on the object-oriented paradigm. Whether it is object-oriented or functional, they are both programming paradigms, that is, different coding styles. The essence is to effectively separate logic, control and data. Knowing the essence of programming, we need some more specific principles to guide our behavior, so I introduced the SOLID design principles. Many people regard SOLID as only a design principle in object-oriented, which is very one-sided. In fact, it can be used as General design principles. On the basis of design principles, we also introduced that design patterns are actually 23 specific cases where design principles are applied.

Good code is inseparable from design. If you don't understand design at all, good code will be impossible to talk about. But as the project progresses, technical debt will gradually accumulate whether the developer is intentional or unintentional, prudent or sloppy. In the same way that people go bankrupt with too much debt, technical debt reaches a certain level and the project cannot continue, so we have to pay down debt from time to time. The way to pay off debt is to identify bad smells and then refactor in a targeted manner.

The content of this article is a collection of practical and effective experience summed up by predecessors in the field of software engineering. I have linked these familiar and unfamiliar concepts together through an example, in order to give you a macro understanding of these concepts. If you think the article is helpful to you, you are welcome to forward and share.

img

Guess you like

Origin blog.csdn.net/CODING_devops/article/details/132088747