第 16 章 修改现有的代码
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.
第 1 章介绍了软件开发是如何迭代和增量的。大型软件系统是通过一系列演化阶段开发的,其中每个阶段都添加了新功能并修改了现有模块。这意味着系统的设计在不断发展。不可能从一开始就为系统构思出正确的设计。一个成熟的系统的设计更多地取决于系统演化过程中所做的更改,而不是任何初始概念。前面的章节描述了如何在初始设计和实现过程中降低复杂性。本章讨论如何防止随着系统的发展而蔓延。
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.
第 3 章介绍了战术编程和战略编程之间的区别:在战术编程中,主要目标是使某些事物快速工作,即使这会导致额外的复杂性;在战略编程中,最重要的目标是进行出色的系统设计。战术方法很快导致系统设计混乱。如果您想要一个易于维护和增强的系统,那么能“工作”并不是一个足够高的标准。您必须优先考虑设计并进行战略思考。当您修改现有代码时,此想法也适用。
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.
不幸的是,当开发人员进入现有代码以进行更改(例如错误修复或新功能)时,他们通常不会从战略角度进行思考。一个典型的心态是“我能做出我需要做的最小的改变是什么?” 有时开发人员认为这是合理的,因为他们对修改的代码不放心。他们担心较大的更改会带来更大的风险,会引入新的错误。然而,这导致了战术编程。每一个最小的变化都会引入一些特殊情况,依赖性或其他形式的复杂性。结果,系统设计变得更糟,并且问题随着系统演进的每一步骤而累积。
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.
如果要保持系统的简洁设计,则在修改现有代码时必须采取战略性方法。理想情况下,当您完成每次更改时,系统将具有如果你在一开始设计时就考虑这一变化而具有的结构。为了实现此目标,您必须抵制诱惑以快速解决问题。相反,请根据所需的更改来考虑当前的系统设计是否仍然是最佳的。如果不是,请重构系统,以便最终获得最佳设计。通过这种方法,每次修改都会改善系统设计。
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.
这也是第 15 页介绍的投资心态的一个示例:如果您花费一些额外的时间来重构和改善系统设计,您将得到一个更干净的系统。这将加快开发速度,您将收回在重构方面投入的精力。即使您的特定更改不需要重构,您仍然应该注意在代码中可以修复的设计缺陷。每当您修改任何代码时,都尝试在该过程中至少找到一点方法来改进系统设计。如果您没有使设计变得更好,则可能会使它变得更糟。
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.
如第 3 章所述,投资心态有时与商业软件开发的现实相冲突。如果“正确的方式”重构系统需要三个月,而快速且肮脏的修复仅需两个小时,则您可能必须采取快速而肮脏的方法,尤其是在紧迫的期限内工作时。或者,如果重构系统会造成不兼容,从而影响许多其他人员和团队,则重构可能不切实际。
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.
尽管如此,您应尽可能抵制这些妥协。问问自己:“考虑到我目前的限制,这是否是我能做的最好的工作来创建一个干净的系统设计?” 也许有一种替代方法几乎可以像 3 个月的重构一样干净,但是可以在几天内完成?或者,如果您现在没有能力做大规模的重构,请让您的老板为您分配时间,让您在当前截止日期之后再来做。每个开发组织都应计划将其全部工作的一小部分用于清理和重构;从长远来看,这项工作将为自己带来回报。
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.
当您更改现有代码时,更改很有可能会使某些现有注释无效。修改代码时,很容易忘记更新注释,从而导致注释不再准确。不准确的注释使读者感到沮丧,如果有很多这样的注释,读者就会开始不信任所有注释。幸运的是,只要有一点纪律和一些指导规则,就可以在不费吹灰之力使注释保持更新。本节及随后的部分提出了一些具体的技巧。
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.
确保注释更新的最佳方法是将注释放置在它们描述的代码附近,以便开发人员在更改代码时可以看到它们。注释离其关联的代码越远,正确更新的可能性就越小。例如,方法接口注释的最佳位置是在代码文件中,紧靠该方法主体的位置。对方法的任何更改都将涉及此代码,因此开发人员很可能会看到接口注释,并在需要时进行更新。
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.
对于 C 和 C++ 等具有单独的代码和头文件的语言,一种替代方法是将接口注释放在.h 文件中方法声明的旁边。但是,这距离代码还有很长的路要走。开发人员在修改方法的主体时将看不到这些注释,因此需要打开其他文件并查找接口注释来更新它们,这需要额外的工作。有人可能会争辩说接口注释应该放在头文件中,以便用户可以学习如何使用一个抽象概念,而不需要查看代码。然而,用户不应该阅读代码或头文件;他们应该从由 Doxygen 或 Javadoc 等工具编译的文档中获取信息。此外,许多 IDE 都会提取文档并将其呈现给用户,例如在键入方法名称时显示方法的文档。鉴于这样的工具,文档应位于对开发人员进行代码开发最方便的位置。
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:
在编写实现注释时,不要将整个方法的所有注释放在方法的顶部。把他们分解开来,将每个注释向下写到最合适范围,即包括该注释所引用的所有代码。例如,如果一种方法具有三个主要阶段,则不要在方法的顶部写一个详细描述所有阶段的注释。而是为每个阶段编写一个单独的注释,并将该注释放置在该阶段的第一行代码的正上方。另一方面,在描述总体策略的方法实现的顶部添加注释也可能会有所帮助,例如:
// 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.
每个阶段的代码上方都可以记录其他详细信息。
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).
通常,注释离描述的代码越远,注释应该越抽象(这减少了注释因代码更改而无效的可能性)。
16.3 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 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.
在编写提交消息时,请问自己将来开发人员是否需要使用该信息。如果是这样,则在代码中记录此信息。一个示例是提交消息,描述了导致代码更改的细微问题。如果代码中未对此进行记录,那么一个开发人员可能会稍后撤消这个更改,而没有意识到他们已经重新创建了错误。如果您也想在提交消息中包含此信息的副本,那也可以,但是最重要的是把他放在代码中。这说明了将文档放置在开发人员最有可能看到它的地方的原则;尽量少放在提交日志中。
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.
保持注释最新的第二种技术是避免重复。如果文档重复,那么开发人员将很难找到并更新所有相关副本。因此尽量将每个设计决策精确的记录一次。如果代码中有多个地方受某个特定决定的影响,请不要在所有这些地方重复注释。所以找到放置注释最明显的位置。例如,假设存在与变量相关的棘手行为,这会影响使用变量的几个不同地方。您可以在变量声明旁边的注释中记录该行为。如果开发人员在理解使用该变量的代码时遇到麻烦,他们自然会在这里进行检查。
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.
如果没有一个“明显的”地方来将特定的文档放在开发人员可以找到的地方,那么创建一个 designNotes 文件,如第 13.7 节所述。或者,在现有的地方中选择一个最好的地方,把文档放在那里。此外,在引用中心位置的其他地方添加简短的注释:“查看 xyz 中的注释以了解下面代码的解释。“如果引用因为主注释被移动或删除而使引用变得过时,这种不一致性将是不言而喻的,因为开发人员将无法在指定的位置找到注释;他们可以使用修订控制历史记录来查找注释发生了什么,然后更新引用。相反,如果文档是重复的,而一些副本没有得到更新,那么开发人员就不会知道他们使用的是陈旧的信息。
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.
不要在另一个模块中记录一个模块的设计决策。例如,不要在方法调用前添加注释,以解释被调用方法中发生的情况。如果读者想知道,他们应该查看该方法的接口注释。好的开发工具通常会自动提供此信息,例如,如果您选择了方法的名称或将鼠标悬停在该方法的名称上,则将显示该方法的接口注释。尽量让开发人员容易找到合适的文档,但是不要通过重复文档来做到这一点。
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:
如果信息已经在程序之外的某个地方记录了,不要在程序内部重复记录;只需参考外部文档。例如,如果您编写一个实现 HTTP 协议的类,那么就不需要在代码中描述 HTTP 协议。在网上已经有很多关于这个文档的来源;只需在您的代码中添加一个简短的注释,并为其中一个源添加一个 URL。另一个例子是已经在用户手册中记录的特性。假设您正在编写一个实现命令集合的程序,其中有一个负责实现每个命令的方法。如果有描述这些命令的用户手册,就不需要在代码中重复这些信息。相反,在每个命令方法的接口注释中包含如下简短说明:
// 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.
读者可以轻松找到理解代码所需的所有文档,这一点很重要,但这并不意味着您必须编写所有这些文档。
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.
确保文档保持最新状态的一种好方法是,在将更改提交到修订控制系统之前需要花费几分钟,以扫描该提交的所有更改。确保文档中正确反映了每个更改。这些预先提交的扫描还将检测其他一些问题,例如意外地将调试代码留在系统中或未完成的 TODO 项目。
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.
关于维护文档的最后一个想法:如果注释比代码更高级,更抽象,则注释更易于维护。这些注释不反映代码的详细信息,因此它们不会受到代码更改的影响;只有整体行为的变化才会影响这些评论。当然,正如第 13 章所讨论的那样,某些注释的确需要详细和精确。但总的来说,最有用的注释(它们不只是重复代码)也最容易维护。