软件设计的哲学:第十六章 修改现有代码

第1章描述了软件开发是如何迭代和增量的。大型软件系统的开发经历了一系列的演化阶段,每个阶段都添加了新的功能并修改了现有的模块。这意味着系统的设计是不断发展的。不可能在一开始就构思出一个系统的正确设计;一个成熟系统的设计更多地取决于系统发展过程中的变化,而不是最初的概念。前几章描述了如何在初始设计和实现过程中挤出复杂性;本章讨论了如何防止复杂性随着系统的发展而增加。

16.1 保持战略

第3章介绍了战术编程和战略编程的区别:在战术编程中,主要目标是让某些东西快速工作,即使这会导致额外的复杂性;在战略规划中,最重要的目标是产生一个伟大的系统设计。战术方法很快就会导致混乱的系统设计。如果你想要一个易于维护和增强的系统,那么“工作”并不是一个足够高的标准;你必须优先考虑设计并有策略地思考。这种思想也适用于修改现有代码时。

不幸的是,当开发人员深入到现有的代码中进行诸如bug修复或新特性之类的更改时,他们通常不会进行战略性思考。一个典型的心态是“我能做的最小的改变是什么来满足我的需要?”“有时候开发人员这么做是因为他们不喜欢修改代码;他们担心更大的变化会带来更大的引入新bug的风险。然而,这导致了战术规划。这些最小的更改中的每一个都引入了一些特殊的情况、依赖项或其他形式的复杂性。结果,系统设计变得更糟了,问题在系统演进的每一步中不断累积。

如果您想要维护一个干净的系统设计,您必须在修改现有代码时采取策略方法。 理想情况下,当您完成了每个更改时,系统将具有如果您从一开始就在头脑中进行设计时所具有的结构。为了实现这个目标,您必须抵制快速修复的诱惑。相反,考虑当前的系统设计是否仍然是最好的,根据需要的更改。如果没有,重构系统,以得到最好的设计。通过这种方法,系统设计可以随着每一次修改而改进。

这也是第15页介绍的投资思维模式的一个例子:如果您投入一点额外的时间来重构和改进系统设计,您将得到一个更简洁的系统。这将加快开发速度,并且您将收回您在重构中投入的精力。即使您的特定更改不需要重构,您也应该在代码中寻找可以修复的设计缺陷。无论何时修改任何代码,都要设法改进系统设计,至少在这个过程中改进一点。如果您没有使设计变得更好,那么您可能使它变得更糟。

正如在第3章中所讨论的,投资心态有时与商业软件开发的现实相冲突。如果以“正确的方式”重构系统需要3个月的时间,而快速而糟糕的修复只需要2个小时,那么您可能不得不采取快速而糟糕的方法,特别是在您的工作时间紧迫的情况下。或者,如果重构系统会造成不兼容,从而影响许多其他人员和团队,那么重构可能是不实际的。

尽管如此,您应该尽可能地抵制这些妥协。问问你自己:“考虑到我目前的限制,这是我所能做的最好的创建一个干净的系统设计吗?”也许有一种方法可以像3个月的重构那样简洁,但可以在几天内完成?或者,如果你现在无法承担大规模重构的费用,让你的老板给你分配时间,让你在当前的最后期限之后再做重构。每个开发组织都应该计划将其全部工作的一小部分用于清理和重构,这项工作从长远来看是值得的。

16.2 维护注释:将注释放在代码附近

当您更改现有代码时,很可能会使某些现有注释失效。修改代码时很容易忘记更新注释,这将导致注释不再准确。不准确的注释会让读者感到沮丧,如果注释太多,读者就会开始怀疑所有的注释。幸运的是,只要有一点规则和一些指导原则,就可以在不付出巨大努力的情况下更新注释。本节和以下各节提出了一些具体的技术。

确保注释得到更新的最佳方法是将它们放置在它们所描述的代码附近,以便开发人员在更改代码时能够看到它们。 注释离相关代码越远,正确更新它的可能性就越小。例如,方法接口注释的最佳位置是在代码文件中,就在方法体旁边。对方法的任何更改都将涉及这段代码,因此开发人员可能会看到接口注释并在需要时更新它们。

对于像C和c++这样具有独立代码和头文件的语言,另一种替代方法是将接口注释放在.h文件中方法声明的旁边。然而,这是一个很长的路从代码;开发人员在修改方法主体时不会看到这些注释,而且需要额外的工作来打开不同的文件并找到接口注释来更新它们。有些人可能认为接口注释应该放在头文件中,这样用户就可以学习如何使用抽象,而不必查看代码文件。但是,用户不需要读取代码或头文件;他们应该从Doxygen或Javadoc等工具编译的文档中获取信息。此外,许多ide将提取并向用户显示文档,例如在键入方法名称时显示方法的文档。对于这样的工具,文档应该放在对开发人员编写代码最方便的地方。

在编写实现注释时,不要将整个方法的所有注释放在方法的顶部。将它们展开,将每个注释下推到最窄的范围,包括注释所引用的所有代码。例如,如果一个方法有三个主要的阶段,不要在方法的顶部写一个注释来详细描述所有的阶段。相反,为每个阶段编写一个单独的注释,并将该注释置于该阶段的第一行代码之上。另一方面,在一个方法的实现顶部有一个描述整体策略的注释也是有帮助的,就像这样:

//  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

16.3 注释属于代码,而不是提交日志

在修改代码时,一个常见的错误是在源代码存储库的提交消息中放入关于更改的详细信息,而不是在代码中记录它。尽管将来可以通过扫描存储库日志来浏览提交消息,但是需要这些信息的开发人员不太可能想到要扫描存储库日志。即使他们确实扫描日志,查找正确的日志消息也会很繁琐。

在编写提交消息时,问问自己将来开发人员是否需要使用这些信息。如果是,那么在代码中记录这些信息。提交消息就是一个例子,它描述了一个引起代码更改的微妙问题。如果代码中没有对此进行记录,那么开发人员可能稍后出现并撤消更改,而没有意识到他们重新创建了一个错误。如果您也想在提交消息中包含此信息的副本,那很好,但最重要的是在代码中获得它。这说明了将 文档放在开发人员最可能看到的地方的原则;提交日志很少在那个地方。

16.4 保留注释:避免重复

使注释保持最新的第二种技术是避免重复。如果文档是重复的,开发人员就很难找到并更新所有相关的副本。相反,尽量只记录一次每个设计决策。如果代码中有多个地方受到某个特定决策的影响,那么不要在每个地方重复文档。相反,找到最明显的地方放置文档。例如,假设有一个与变量相关的复杂行为,它会影响变量使用的几个不同位置。您可以在变量声明旁边的注释中记录该行为。如果开发人员在理解使用该变量的代码时遇到困难,这是一个很自然的地方。

如果没有一个“明显的”地方来放置特定的文档,开发人员可以找到它,那么创建一个designNotes文件,如第13.7节所述。或者,选择最好的地方,把文档放在那里。另外,在引用中心位置的其他地方添加简短的注释:“查看xyz中的注释以了解下面代码的解释。“如果引用因为主注释被移动或删除而变得过时,这种不一致性将是不言而喻的,因为开发人员不会在指定的地方找到注释;他们可以使用修订控制历史记录来查找注释发生了什么,然后更新引用。相反,如果文档是重复的,并且一些副本没有得到更新,那么开发人员就不会知道他们使用的是陈旧的信息。

如果信息已经在程序之外的某个地方记录了,不要在程序内部重复记录;只需参考外部文档。例如,如果您编写一个实现HTTP协议的类,那么就不需要在代码中描述HTTP协议。在网上已经有很多关于这个文档的来源;只需在您的代码中添加一个简短的注释,并为其中一个源添加一个URL。另一个例子是已经在用户手册中记录的特性。假设您正在编写一个实现命令集合的程序,其中有一个方法负责实现每个命令。如果有描述这些命令的用户手册,就不需要在代码中重复这些信息。相反,在每个命令方法的接口注释中包含如下简短说明:

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

重要的是读者可以很容易地找到所有需要的文档来理解您的代码,但这并不意味着您必须编写所有的文档。

不要在另一个模块中重新记录一个模块的设计决策。例如,不要在一个方法调用之前加上注释来解释在被调用的方法中发生了什么。如果读者想知道,他们应该查看方法的接口注释。好的开发工具通常会自动提供这些信息,例如,如果您选择了一个方法的名称或者将鼠标悬停在它上面,就会显示该方法的接口注释。尽量让开发人员容易地找到适当的文档,但不要重复文档。

16.5 维护注释:检查差异

确保文档保持最新的一个好方法是,在将更改提交到修订控制系统之前花几分钟时间扫描提交的所有更改,确保每个更改都正确地反映在文档中。 这些预提交扫描还将检测其他几个问题,如意外地在系统中留下调试代码或未能修复TODO项。

16.6 更高级别的注释更容易维护

关于维护文档的最后一个想法:如果注释比代码更高级、更抽象,那么它们更容易维护。这些注释不反映代码的细节,因此它们不会受到代码的细微更改的影响;只有整体行为的改变才会影响这些注释。当然,正如第13章所讨论的,有些注释确实需要详细和精确。但是一般来说,最有用的注释(它们不会简单地重复代码)也是最容易维护的。

posted @ 2019-12-27 13:40  peida  阅读(421)  评论(0编辑  收藏