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

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

16.1 保持战略

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

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

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

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

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

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

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

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

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

对于像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章所讨论的,有些注释确实须要详细和精确。可是通常来讲,最有用的注释(它们不会简单地重复代码)也是最容易维护的。

相关文章
相关标签/搜索