"The Philosophy of Software Design" (17) [Modify the existing code]

Chapter 16 Modifying Existing Code

Chapter 16 Modifying Existing Code

Chapter 1 described how software development is iterative and incremental. A large software system develops through a series of evolutionary stages, where each stage adds new capabilities and modifies existing modules. This means that a system’s design is constantly evolving. It isn’t possible to conceive the right design for a system at the outset; the design of a mature system is determined more by changes made during the system’s evolution than by any initial conception. Previous chapters described how to squeeze out complexity during the initial design and implementation; this chapter discusses how to keep complexity from creeping in as the system evolves.

Chapter 1 describes how software development is iterative and incremental. Large-scale software systems are developed through a series of evolutionary stages, each of which adds new features and modifies existing modules. This means that the design of the system is constantly evolving. It is impossible to design the correct design for the system from the beginning. The design of a mature system depends more on the changes made during the evolution of the system, rather than any initial concepts. The previous chapters described how to reduce complexity during initial design and implementation. This chapter discusses how to prevent the increase in complexity as the system evolves.

16.1 Stay strategic

Chapter 3 introduced the distinction between tactical programming and strategic programming: in tactical programming, the primary goal is to get something working quickly, even if that results in additional complexity; in strategic programming, the most important goal is to produce a great system design. The tactical approach very quickly leads to a messy system design. If you want to have a system that is easy to maintain and enhance, then “working” isn’t a high enough standard; you have to prioritize design and think strategically. This idea also applies when you are modifying existing code.

Chapter 3 introduces the difference between tactical programming and strategic programming: In tactical programming, the main goal is to make certain things work quickly, even if this leads to additional complexity; in strategic programming, the most important goal is to perform Excellent system design. The tactical approach quickly led to confusion in the system design. If you want a system that is easy to maintain and enhance, then the "work" is not high enough. You must prioritize design and think strategically. This idea also applies when you modify existing code.

Unfortunately, when developers go into existing code to make changes such as bug fixes or new features, they don’t usually think strategically. A typical mindset is “what is the smallest possible change I can make that does what I need?” Sometimes developers justify this because they are not comfortable with the code being modified; they worry that larger changes carry a greater risk of introducing new bugs. However, this results in tactical programming. Each one of these minimal changes introduces a few special cases, dependencies, or other forms of complexity. As a result, the system design gets just a bit worse, and the problems accumulate with each step in the system’s evolution.

Unfortunately, when developers enter existing code to make changes (such as bug fixes or new features), they usually don’t think strategically. A typical mentality is "What is the smallest change I need to make?" Sometimes, developers justify this because they are not satisfied with the modified code. They worry that larger changes will bring a greater risk of introducing new errors. However, this led to tactical programming. Each of these minimal changes will introduce some special circumstances, dependencies or other forms of complexity. As a result, the system design becomes worse, and problems accumulate with each step of the system development.

If you want to maintain a clean design for a system, you must take a strategic approach when modifying existing code. Ideally, when you have finished with each change, the system will have the structure it would have had if you had designed it from the start with that change in mind. To achieve this goal, you must resist the temptation to make a quick fix. Instead, think about whether the current system design is still the best one, in light of the desired change. If not, refactor the system so that you end up with the best possible design. With this approach, the system design improves with every modification.

If you want to maintain a simple design of the system, you must take a strategic approach when modifying existing code. Ideally, when you complete each change, if you design the system with the change in mind from the beginning, the system will have the structure it should have. In order to achieve this goal, you must resist the temptation to solve the problem quickly. Instead, consider whether the current system design is still optimal based on the changes required. If not, please refactor the system to get the best design in the end. In this way, every modification will improve the system design.

This is also an example of the investment mindset introduced on page 15: if you invest a little extra time to refactor and improve the system design, you’ll end up with a cleaner system. This will speed up development, and you will recoup the effort that you invested in the refactoring. Even if your particular change doesn’t require refactoring, you should still be on the lookout for design imperfections that you can fix while you’re in the code. Whenever you modify any code, try to find a way to improve the system design at least a little bit in the process. If you’re not making the design better, you are probably making it worse.

This is also an example of the investment mentality described on page 15: if you spend some extra time refactoring and improving the system design, you will get a cleaner system. This will speed up development and you will recoup the effort invested in refactoring. Even if your specific changes do not require refactoring, you should still be aware of design flaws that can be fixed in the code. Whenever you modify any code, try to find at least one way in the process to improve the system design. If you do not make the design better, you may make it worse.

As discussed in Chapter 3, an investment mindset sometimes conflicts with the realities of commercial software development. If refactoring the system “the right way” would take three months but a quick and dirty fix would take only two hours, you may have to take the quick and dirty approach, particularly if you are working against a tight deadline. Or, if refactoring the system would create incompatibilities that affect many other people and teams, then the refactoring may not be practical.

As mentioned in Chapter 3, the investment mentality sometimes conflicts with the reality of commercial software development. If the "right way" to rebuild the system takes three months, and a quick and dirty repair only takes two hours, you may have to take a quick and dirty approach, especially when working within tight deadlines. Or, if refactoring the system would cause incompatibility that affects many other people and teams, refactoring may be impractical.

Nonetheless, you should resist these compromises as much as possible. Ask yourself “Is this the best I can possibly do to create a clean system design, given my current constraints?” Perhaps there’s an alternative approach that would be almost as clean as the 3-month refactoring but could be done in a couple of days? Or, if you can’t afford to do a large refactoring now, get your boss to allocate time for you to come back to it after the current deadline. Every development organization should plan to spend a small fraction of its total effort on cleanup and refactoring; this work will pay for itself over the long run.

However, you should resist these compromises as much as possible. Ask yourself: "Considering my current limitations, is this the best work I can do to create a clean system design?" There may be an alternative approach that can be almost as clean as 3 months of refactoring, but Can it be completed in a few days? Or, if you can’t afford a major refactoring right now, ask your boss to allocate time for you to get you back to your original level after the current deadline. Every development organization should plan to spend a small part of its entire work for cleanup and refactoring; in the long run, this work will pay for itself.

16.2 Maintaining comments: keep the comments near the code

When you change existing code, there’s a good chance that the changes will invalidate some of the existing comments. It’s easy to forget to update comments when you modify code, which results in comments that are no longer accurate. Inaccurate comments are frustrating to readers, and if there are very many of them, readers begin to distrust all of the comments. Fortunately, with a little discipline and a couple of guiding rules, it’s possible to keep comments up-to-date without a huge effort. This section and the following ones put forth some specific techniques.

When you change existing code, the change is likely to invalidate some existing comments. When you modify the code, it is easy to forget to update the comments, resulting in the comments being no longer accurate. Inaccurate comments frustrate readers. If there are too many comments, readers will start to distrust all comments. Fortunately, with a little discipline and some guidelines, you can keep the notes up-to-date without great effort. This section and subsequent sections propose some specific techniques.

The best way to ensure that comments get updated is to position them close to the code they describe, so developers will see them when they change the code. The farther a comment is from its associated code, the less likely it is that it will be updated properly. For example, the best place for a method’s interface comment is in the code file, right next to the body of the method. Any changes to the method will involve this code, so the developer is likely to see the interface comments and update them if needed.

The best way to ensure that comments are updated is to place comments near the code they describe so that developers can see them when making changes to the code. The farther a comment is from its associated code, the less likely it is to update it correctly. For example, the best location for method interface comments is in the code file, next to the method body. Any changes to the method will involve this code, so developers are likely to see the interface comments and update them if needed.

An alternative for languages like C and C++ that have separate code and header files, is to place the interface comments next to the method’s declaration in the .h file. However, this is a long way from the code; developers won’t see those comments when modifying the method’s body, and it takes additional work to open a different file and find the interface comments to update them. Some might argue that interface comments should go in header files so that users can learn how to use an abstraction without having to look at the code file. However, users should not need to read either code or header files; they should get their information from documentation compiled by tools such as Doxygen or Javadoc. In addition, many IDEs will extract and present documentation to users, such as by displaying a method’s documentation when the method’s name is typed. Given tools such as these, the documentation should be located in the place that is most convenient for developers working on the code.

For languages ​​with separate code and header files, such as C and C++, an alternative is to put interface comments next to the method declaration in the .h file. However, this is still a long way from code. Developers will not see these comments when modifying the body of the method, so they need to open other files and look for interface comments to update them, which requires additional work. Someone might argue that interface comments should be placed in header files so that users can learn how to use abstractions without having to look at the code files. However, users do not need to read code or header files; they should obtain information from documentation compiled by tools such as Doxygen or Javadoc. In addition, many IDEs extract the documentation and present it to the user, such as displaying the documentation of the method when typing the method name. Given a tool such as this, the documentation should be in the most convenient location for developers to develop code.

When writing implementation comments, don’t put all the comments for an entire method at the top of the method. Spread them out, pushing each comment down to the narrowest scope that includes all of the code referred to by the comment. For example, if a method has three major phases, don’t write one comment at the top of the method that describes all of the phases in detail. Instead, write a separate comment for each phase and position that comment just above the first line of code in that phase. On the other hand, it can also be helpful to have a comment at the top of a method’s implementation that describes the overall strategy, like this:

When writing implementation comments, do not put all the comments of the entire method at the top of the method. Expand them to push each comment to its narrowest scope, which includes all the code referenced by that comment. For example, if a method has three main stages, don't write a note at the top of the method that describes all the stages in detail. Instead, write a separate comment for each stage and place the comment directly above the first line of code in that stage. On the other hand, it may also be helpful to add comments at the top of the method implementation describing the overall strategy, for example:

//  We proceed in three phases:
//  Phase 1: Find feasible candidates
//  Phase 2: Assign each candidate a score
//  Phase 3: Choose the best, and remove it

Additional details can be documented just above the code for each phase.

Other detailed information can be recorded above the code of each stage.

In general, the farther a comment is from the code it describes, the more abstract it should be (this reduces the likelihood that the comment will be invalidated by code changes).

In general, the farther a comment is from the described code, the more abstract the comment should be (this reduces the possibility that the comment becomes invalid due to code changes).

16.3 Comments belong in the code, not the commit log Comments belong in the code, not the commit log

A common mistake when modifying code is to put detailed information about the change in the commit message for the source code repository, but then not to document it in the code. Although commit messages can be browsed in the future by scanning the repository’s log, a developer who needs the information is unlikely to think of scanning the repository log. Even if they do scan the log, it will be tedious to find the right log message.

When modifying code, a common mistake is to put detailed information about the change in the commit message of the source code repository instead of recording it in the code. Although it is possible to browse the commit messages by scanning the repository's logs in the future, developers who need this information are unlikely to consider scanning the repository's logs. Even if they do scan the log, it is difficult to find the correct log message.

When writing a commit message, ask yourself whether developers will need to use that information in the future. If so, then document this information in the code. An example is a commit message describing a subtle problem that motivated a code change. If this isn’t documented in the code, then a developer might come along later and undo the change without realizing that they have re-created a bug. If you want to include a copy of this information in the commit message as well, that’s fine, but the most important thing is to get it in the code. This illustrates the principle of placing documentation in the place where developers are most likely to see it; the commit log is rarely that place.

When writing a commit message, ask yourself if the developer needs to use the information in the future. If so, record this information in the code. An example is a commit message, which describes the subtle issue that caused the code change. If this is not documented in the code, the developer may propose and undo the change later without realizing that they have recreated the error. If you also want to include a copy of this information in the commit message, that's great, but the most important thing is to get it in code. This illustrates the principle of placing the document where the developer is most likely to see it; the commit log is rarely in that place.

16.4 Maintaining comments: avoid duplication

The second technique for keeping comments up to date is to avoid duplication. If documentation is duplicated, it is more difficult for developers to find and update all of the relevant copies. Instead, try to document each design decision exactly once. If there are multiple places in the code that are affected by a particular decision, don’t repeat the documentation at each of these points. Instead, find the most obvious single place to put the documentation. For example, suppose there is tricky behavior related to a variable, which affects several different places where the variable is used. You can document that behavior in the comment next to the variable’s declaration. This is a natural place that developers are likely to check if they’re having trouble understanding code that uses the variable.

The second technique for keeping comments up to date is to avoid duplication. If the document is duplicated, it will be difficult for developers to find and update all relevant copies. Instead, try to record each design decision only once. If there are multiple places in the code that are affected by a particular decision, please don’t repeat the documentation in all these places. Instead, find the most obvious place to place the document. For example, suppose there are tricky behaviors related to variables, which affect several different places where the variables are used. You can record this behavior in the comment next to the variable declaration. This is natural, and developers may check if they have trouble understanding the code that uses the variable.

If there is no “obvious” single place to put a particular piece of documentation where developers will find it, create a designNotes file as described in Section 13.7. Or, pick the best of the available places and put the documentation there. In addition, add short comments in the other places that refer to the central location: “See the comment in xyz for an explanation of the code below.” If the reference becomes obsolete because the master comment was moved or deleted, this inconsistency will be self-evident because developers won’t find the comment at the indicated place; they can use revision control history to find out what happened to the comment and then update the reference. In contrast, if the documentation is duplicated and some of the copies don’t get updated, there will be no indication to developers that they are using stale information.

If there is no "obvious" place to put a particular document so that the developer can find it, then create a designNotes file, as described in Section 13.7. Or, choose the best place and put the document there. In addition, add a short comment elsewhere in the center of the reference: "Check the comments in xyz to understand the explanation of the code below." If the reference becomes obsolete because the main comment is moved or deleted, this inconsistency will be self-evident Metaphorically, because developers will not be able to find the comment in the specified location; they can use revision control history to find what happened to the comment and then update the reference. Conversely, if the document is duplicated and some copies have not been updated, then developers will not know that they are using outdated information.

Don’t redocument one module’s design decisions in another module. For example, don’t put comments before a method call that explain what happens in the called method. If readers want to know, they should look at the interface comments for the method. Good development tools will usually provide this information automatically, for example, by displaying the interface comments for a method if you select the method’s name or hover the mouse over it. Try to make it easy for developers to find appropriate documentation, but don’t do it by repeating the documentation.

Do not record the design decisions of one module in another module. For example, do not add comments before the method call to explain what happens in the called method. If readers want to know, they should check the interface notes for the method. Good development tools usually provide this information automatically, for example, if you select the name of a method or hover the mouse over the name of the method, the interface comment of the method will be displayed. Try to make it easy for developers to find the appropriate documentation, but don't repeat the documentation.

If information is already documented someplace outside your program, don’t repeat the documentation inside the program; just reference the external documentation. For example, if you write a class that implements the HTTP protocol, there’s no need for you to describe the HTTP protocol inside your code. There are already numerous sources for this documentation on the Web; just add a short comment to your code with a URL for one of these sources. Another example is features that are already documented in a user manual. Suppose you are writing a program that implements a collection of commands, with one method responsible for implementing each command. If there is a user manual that describes those commands, there’s no need to duplicate this information in the code. Instead, include a short note like the following in the interface comment for each command method:

If the information has already been recorded somewhere outside the program, do not record it repeatedly inside the program; just refer to the external document. For example, if you write a class that implements the HTTP protocol, you do not need to describe the HTTP protocol in the code. There are already many sources of this document on the Internet; just add a short comment to your code and add a URL to one of the sources. Another example is the characteristics that have been documented in the user manual. Suppose you are writing a program that implements a set of commands, and there is a method responsible for implementing each command. If there is a user manual describing these commands, there is no need to repeat this information in the code. Instead, include the following brief description in the interface comment of each command method:

// Implements the Foo command; see the user manual for details.

It’s important that readers can easily find all the documentation needed to understand your code, but that doesn’t mean you have to write all of that documentation.

Readers can easily find all the documents needed to understand the code, which is important, but it does not mean that you have to write all of these documents.

16.5 Maintaining comments: check the diffs

One good way to make sure documentation stays up to date is to take a few minutes before committing a change to your revision control system to scan over all the changes for that commit; make sure that each change is properly reflected in the documentation. These pre-commit scans will also detect several other problems, such as accidentally leaving debugging code in the system or failing to fix TODO items.

A good way to ensure that the document is kept up-to-date is to take a few minutes to scan all changes to that submission before submitting the changes to the revision control system. Ensure that each change is correctly reflected in the documentation. These pre-committed scans will also detect other issues, such as accidentally leaving debugging code in the system or failing to repair TODO items.

16.6 Higher-level comments are easier to maintain

One final thought on maintaining documentation: comments are easier to maintain if they are higher-level and more abstract than the code. These comments do not reflect the details of the code, so they will not be affected by minor code changes; only changes in overall behavior will affect these comments. Of course, as discussed in Chapter 13, some comments do need to be detailed and precise. But in general, the comments that are most useful (they don’t simply repeat the code) are also easiest to maintain.

One final thought on maintaining documentation: If comments are more advanced and abstract than code, comments are easier to maintain. These comments do not reflect the details of the code, so they will not be affected by code changes; only changes in overall behavior will affect these comments. Of course, as discussed in Chapter 13, certain notes do need to be detailed and precise. But in general, the most useful comments (they are not just repetitive code) are also the easiest to maintain.

Guess you like

Origin blog.csdn.net/WuLex/article/details/108617956