What are some good ways to improve code quality? What experience and skills do you have?

6 high-level strategies for ensuring code quality:

1 Write easy-to-understand code

Consider the following text. We've intentionally made it difficult to understand, so don't waste too much time deciphering it. Read it briefly and absorb as much as you can.

〓ts〓Take a bowl, let's call it A for now. Take a pan, let's call it B for now. Fill B with water and place on hob. Put butter and chocolate in A, 100g of the former and 185g of the latter. This should be 70% dark chocolate. Place A on top of B; wait for the contents of A to melt, then move A out of B. Take another bowl, we'll call it C for now. Put eggs, sugar and vanilla essence in C, put two of the first ingredient, 185g of the second ingredient, and half a teaspoon of the third ingredient. Mix the contents of C. After the contents of A have cooled, add them to C and mix. Take a bowl, let's call it D. Put flour, cocoa powder and salt in D, 50g of the first ingredient, 35g of the second ingredient, and half a teaspoon of the third ingredient. Mix the contents of D thoroughly, then strain into C. Stir the contents of D well to combine completely. We're going to make brownies this way, did I forget to mention that? Add 70g of chocolate chips to D and stir the contents of D well. Take a baking mold, let's call it E. Grease and line with baking paper in E. Put the contents of D into E. We'll call your oven F. By the way, you should preheat the F to 160C. Put E into F for 20min, then take it out. Let E cool for a few hours.

Now, let's ask some questions.

〓● What does this text say?

〓● Following these instructions, what can we end up with?

〓● What ingredients do we need? What are the serving sizes of the various ingredients?

We can find answers to the above questions in this text, but not easily. The readability of this text is poor. There are many problems that caused this result, including the following.

〓● There is no title, so we have to read the entire text to get its meaning.

〓● This text is not well composed as a series of steps (or sub-questions), but like a long wall of text.

〓● Refer to things with unhelpful vague names like "A" instead of "bowl with melted cream and chocolate".

〓● Information is far removed from where it is needed: ingredients and quantities are separated from each other, and important instructions like the oven needing to be preheated are mentioned at the end.

(You may have gotten tired of reading this text, which is a brownie recipe. If you really want to make this treat, there's a more accessible version in Appendix A.)

Reading a piece of poor-quality code and trying to grasp its meaning is no different than our experience of just reading a brownie recipe. In particular, it may be difficult for us to understand the following situations about the code:

〓 What to do;

〓 How to do it;

〓● What components are required (input or state);

〓● What do you get after running the code.

At some point, other engineers will most likely have to read and understand our code. This happens almost instantly if our code has to go through a code review before being committed. But even ignoring code reviews, at some point someone else will look at our code and try to understand what it does. This can happen when requirements change or when code needs to be debugged.

If our code is poorly readable, other engineers will have to spend a lot of time deciphering it. There's a good chance they've misunderstood what it does, or left out some important detail. If this happens, defects are less likely to be found during code reviews and more likely to be introduced as others modify our code, adding new features. The functions of the software are all based on the code. If engineers can't understand what the code does, it's nearly impossible to be sure that the software will work. Just like recipes, code must be easy to understand.

In Chapter 2, we'll see how defining the right level of abstraction can help achieve readability. And in Chapter 5, we'll cover some specific techniques for making code easier to understand.

2 Avoid surprises

Getting a present on your birthday, or winning the lottery are examples of unexpected good deeds (surprises). However, surprises are usually not a good thing when we are trying to accomplish a specific task.

Imagine you're hungry and decide to order some pizza. You pull out your phone, find the number for the pizzeria, and dial. Strangely, there was a long silence on the other end of the phone, but it was finally connected, and a voice asked you: What do you want?

"A large margherita pizza, please, for delivery."

"Okay, what's your address?"

Half an hour later, you received the takeaway, opened the package and found the scene in Figure 1-3.

Figure 1-3. If you thought you were talking to a pizzeria but it turned out to be a Mexican restaurant, your order still made sense, but something unexpected might arrive

Wow, what a surprise. Apparently, someone misheard "margherita" (the name of a type of pizza) for "margarita" (the name of a type of cocktail). But that's odd because this pizzeria doesn't serve cocktails.

Turns out, the custom dialer app you use on your phone has added a new "smart" feature. App developers found that when users call one restaurant and the line is busy, 80% call another restaurant right away, so they created a time-saving handy feature: When you call an app recognizes is the restaurant's phone number and it is busy, the app will seamlessly call the next restaurant's number in the phone book.

In this example, the next restaurant happens to be your favorite Mexican restaurant, not the pizzeria you thought you were calling. Mexican restaurants definitely serve margaritas, not pizza. App developers had good intentions and thought that this feature would make life easier for users, but they created a system that would bring some kind of surprise. We rely on our mental models of the phone to determine what happened based on what we heard. Importantly, if we hear a verbal response, the mental model tells us that we are connected to the number we just dialed.

This new feature of the custom dialer app has changed the way the app behaves beyond our expectations. It breaks our mental model assumption that a voice answer means we're connected to the number we just dialed. This feature may be useful, but because it acts outside of the mental model of the average person, people must be clearly informed of what is happening, such as a voice message telling people that the number they are calling is busy, asking if they would like to call another restaurant's number.

This custom dialer application can be compared to a piece of code. Names, data types, and common conventions are used as cues by other engineers using our code to build a mental model of what our code takes as input, what it does, and what it returns. If our code behaves outside of this mental model, it is likely to lead to hidden defects in the software.

In the case of calling the pizzeria, even after the unexpected happened, everything seemed to work: you ordered a margherita pizza, and the restaurant was happy to serve it. It's not until much later, when the mistake is irreparable, that you inadvertently order a cocktail instead of a pizza. This is similar to what often happens when code in a software system does something unexpected: because the caller of the code did not anticipate the surprise, the code continues executing without knowing. For a while, everything looks normal, but then something goes horribly wrong and the program is left in an invalid state, or returns a weird value to the user.

Even with the best of intentions, writing code that provides some useful or "clever" functionality runs the risk of creating surprises. If the code does something unexpected, the engineers using the code don't know or think about how to handle that situation. This tends to cause the system to "limp" until obvious quirks far away from the offending code appear. Perhaps, this just creates a somewhat annoying flaw, but it can also cause catastrophic problems that destroy important data. We should watch out for unexpected situations in our code and avoid them as much as possible.

In Chapter 3, we will see that code contracts are an underlying technique that can help solve this problem. In Chapter 4, when introducing software errors, it is mentioned that if these errors cannot be properly prompted or handled, unexpected situations may result. Chapter 6 will focus on some more specific techniques for avoiding surprises.

3 Write code that is hard to misuse

On the back of the TV, we may see the interface as shown in Figure 1-4. We can plug different cables into these ports. Importantly, TV manufacturers can prevent users from plugging the power cord into the HDMI interface by designing different interfaces into different shapes.

Figure 1-4 TV manufacturers intentionally make the ports on the back of the TV different shapes to prevent users from plugging in wrong cables

Imagine what would happen if TV makers didn't do this and instead made every connector the same shape. How many people do you think accidentally plug a cable into the wrong port while fumbling around the back of their TV? If you plug the HDMI cable into the power outlet, the TV may not work, which is annoying but not too scary. But if someone plugged the power cord into the HDMI port, it would burn the TV's circuit board.

The code we write is often called by other code, it's a bit like the back of a TV. We expect other code to "plug in" something, like input parameters, or put the system in a certain state before the call. If the wrong thing is "inserted" into the code, some damage can be done: the system crashes, the database is permanently damaged, or some important data is lost. Even if no damage is done, there is a good chance that the code will not work correctly. Our code is called for a reason, and inserting something incorrect could mean that an important task wasn't performed, or that some quirk went unnoticed.

By writing code that is difficult or impossible to misuse, we maximize the probability that the code will continue to work correctly. There are many practical solutions to this problem. Code contracts (similar to avoiding surprises), introduced in Chapter 3, are fundamental techniques that help write code that is difficult to misuse. Chapter 7 introduces some more specific techniques for writing code that is difficult to misuse.

4 Write modular code

Modularity means that an object or system is composed of smaller components that can be replaced independently. To illustrate this concept and the benefits of modularity, consider the two toys in Figure 1-5.

Figure 1-5. Modular toys are easy to reconfigure, whereas stitched toys are extremely difficult to reconfigure.

The toy on the left in Figure 1-5 is highly modular. The head, arms, palms and legs are all easily replaceable independently without affecting the rest of the toy. In contrast, the toy on the right side of Figure 1-5 is non-modular. There is no easy way to replace a head, arm, hand or leg.

One of the characteristics of a modular system (such as the toy on the left in Figure 1-5) is that different components have well-defined interfaces, with as few points of interaction as possible. If we consider a palm as a component, the toy on the left has only one interaction point and a simple interface: a nail, and a small hole that fits into it. The toy on the right, on the other hand, has an extremely complex interface between the palm and the rest of the body: more than 20 loops of intertwined threads on the palm and arm.

Now imagine if our task is to maintain these toys, and one day the manager tells us a new requirement: fingers on the palm. Which toy/system do we prefer to deal with?

For the toy on the left, we can manufacture a newly designed palm that easily replaces the existing one. If two weeks later, the manager changes his mind, we can restore the toy to its original configuration without any trouble.

As for the toy on the right, we'll probably have to get out our scissors, snip off those 20+ loops of thread, and sew the new palm directly onto the toy. In the process, we might damage the toy, and if the manager changes his mind after two weeks, we'll have to go to great lengths to restore the toy to its original configuration.

Software systems and code bases are very similar to these toys. It is often beneficial to decompose code into independent modules, where the interaction of two adjacent modules occurs in a single place, using well-defined interfaces. This helps ensure that the code is easier to adapt to changing requirements, since a change in one feature doesn't require extensive changes everywhere.

Modular systems are also generally easier to understand and reason about. Because the system is broken down into small, easily controllable functional blocks, the interactions between functional blocks are clearly defined and documented. This increases the likelihood that the code will work correctly in the first place and continue to work in the future - because engineers are less likely to misunderstand what the code does.

In Chapter 2, we'll look at how to create clear levels of abstraction, a foundational technique that leads to more modular code. In Chapter 8, we'll also look at a series of specific techniques for making code more modular.

5. Write reusable, generalizable code

The concepts of reusability and generalizability are similar but slightly different.

〓●  Reusability means that a system can be used in multiple scenarios to solve the same problem. A hand drill is a reusable tool because it can drill holes in walls, floors, and ceilings. The problem is the same (a hole needs to be drilled), but the scenarios are different (drilling a wall, drilling a floor, and drilling a ceiling).

〓●  Generalizability means that a system can be used to solve multiple problems with similar concepts but slight differences. A hand drill is also a generalizable tool because it can be used to drill a hole and also to fasten a screw to an object. Manufacturers recognize that rotation is a common problem that applies to both turning holes and set screws, so they create tools that work for both.

In the hand drill example, we can immediately see the benefits of these two features. Imagine if we needed 4 different tools.

〓● Drilling machine that can only work in the state of horizontal lifting——can only be used for drilling walls.

〓● A drilling machine that can only work vertically downwards—only for drilling floors.

〓● A drilling machine that can only work vertically upwards—only for drilling ceilings.

〓● An electric screwdriver for fixing the screws.

We have to pay a lot of money for this set of 4 tools, carry more stuff with us, and charge 4 sets of batteries - all a waste. Thanks to someone who invented a hand drill that is both reusable and scalable, we only need one tool to do all of the above. You don't need to guess, the hand drill is another analogy to the code here.

It takes time and effort to create code and, once created, an ongoing investment of time and effort to maintain it. Creating code is not without risk: despite our care, some code we write will contain bugs, and the more we write, the more likely bugs will appear. The point is, the fewer lines of code we leave in the codebase, the better. This may sound strange, don't we get paid to write code? But in fact, we get paid because we can solve a certain problem, and the code is just a means. It would be great if we could fix the problem while spending less effort and reducing the chances that we accidentally introduce a bug that causes other problems.

By writing reusable, generalizable code, we (and others) can use it in multiple places and scenarios in the code base, solving more than one problem. This saves time and effort, and makes our code more reliable, since we tend to reuse logic that has been tried and tested externally, where bugs may have already been found and fixed.

Code that is more modular in nature also tends to be more reusable and generalizable. Chapters on modularity are closely related to the topics of reusability and generalizability. In addition, Chapter 9 introduces some specialized techniques and considerations to improve code reusability and generalizability.

6 Write testable code and test it appropriately

As we saw earlier in the software development and deployment process (see Figure 1-2), testing is a critical part of ensuring that bugs and imperfect functionality are not ultimately put into operation . They tend to be the main safeguards at two critical points in the process (see Figure 1-6).

〓● Prevent defective or incomplete features from being submitted to the code base.

〓● Ensure that defective or incomplete functions are prevented from being released and put into operation.

Therefore, testing is essential to ensure that the code is available and continues to work properly.

Figure 1-6 To minimize bugs and imperfect features from entering the code base and ensure that they are not released to the public, testing is critical

In software development, the importance of testing cannot be overemphasized. You've certainly heard this said so many times before that it's easy to dismiss it as a cliché, but it does matter. As we have seen in many places in this book.

〓● Software systems and code bases are often too large and complex for one person to understand all the details.

〓● Humans (even highly intelligent engineers) make mistakes.

This is more or less a fact of life. Unless we lock down the functionality of our code with tests, those functionality will habitually entangle us (and our code).

This pillar of code quality encompasses two important concepts: "writing testable code" and "proper testing". Testing and testability are related, but take different considerations.

〓●  Testing ——As the name suggests, this is related to testing our code or software. Testing may be performed manually or automatically. As engineers, we usually strive to write test code that executes the "real" code and checks that everything behaves as expected. There are different levels of testing. The 3 most common test levels you might use are as follows. ( Note that this is not a complete list. There are many ways to categorize tests, and different organizations often use different terminology. )

〓❏Unit  testing - This type of testing usually tests small units of code (such as a single function or class). Unit testing is the level of testing most frequently used by test engineers in their day-to-day programming, and it is the only level of testing covered in detail in this book.

〓❏Integration  Testing ——A system usually consists of multiple components, modules or subsystems. The process of connecting these components and subsystems together is called integration . Integration tests try to make sure that those integrations work and keep working.

〓❏End  -to-end (E2E) testing ——a typical process of testing the entire software system from beginning to end. If the software under test is an online shopping system, an example of E2E testing is to automatically drive the browser to ensure that the user can complete a shopping process.

〓Testability  - This refers to "real code" (as opposed to test code) and describes how that code behaves under test . The notion that something is "testable" also applies at the subsystem or system level. Testability is often highly correlated with modularity, the more modular the code (or system) is, the easier it is to test. Imagine an automaker is developing an emergency pedestrian anti-collision braking system. If the system is not very modular, the only way to test it may be to install it in a real car, drive it in front of a real person, and check whether the vehicle will automatically stop. If that's the case, the system can test only a limited number of scenarios because each test is expensive: build a whole car, rent a test road, and have a real person risk playing pedestrians on the road. If this emergency braking system is a separate module that can be run outside the real vehicle, the testability is even higher. Testing can now be done by feeding the system a pre-recorded video of a pedestrian walking out and checking that it outputs the correct signal for the emergency braking system. Such tests are simple, inexpensive and safe, and can be tested in thousands of different pedestrian situations.

If the code is not testable, it is impossible to "properly" test it. In order to ensure that the code we write is testable, it is best to constantly ask ourselves a question when writing code: "How will we test this code?" Indivisible basic components.

recommended books

good code bad code

This book introduces key concepts and techniques that professional software engineers often use to write reliable, maintainable code. This book is not simply a list of "do's" and "don'ts", but aims to explain the core theory behind each concept and technology, as well as the factors that need to be weighed. This should give the reader a basic understanding of how to think and program like an experienced software engineer.

This book shares practical tips for writing code that is robust, reliable, and easy for team members to understand and adapt to. Covers how to think about code like an effective software engineer, how to write functions that read like a well-formed sentence, how to make sure your code is reliable and bug-free; How to make improvements, how to write code that can be reused and adapted to new requirements, how to improve readers' medium and long-term productivity; at the same time, it also introduces how to save valuable time for developers and teams, and so on.

This book is concise in text, incisive in discussion, and clear in hierarchy. It is suitable for developers with zero foundation to read, and it also has high reference value for students of computer and related majors in colleges and universities.

Guess you like

Origin blog.csdn.net/epubit17/article/details/129420742