第 12 章 不写注释的四个借口
代码中的文档在软件设计中起着至关重要的作用。注释能帮助开发人员理解系统、高效工作,但其作用不止于此。文档也是抽象的重要组成部分:没有注释,就无法隐藏复杂性。最后,只要方法得当,编写注释的过程本身就能改善系统设计。 反之,再好的软件设计,缺乏良好文档也会失去很多价值。
本章概览不写注释的四个借口,站得住吗?
遗憾的是,这一观点并未得到普遍认同。相当一部分生产代码几乎没有注释。许多开发人员认为写注释是浪费时间;另一些人虽然看到了注释的价值,却总是迟迟没有动笔。好在许多开发团队认识到了文档的价值,而且这样的团队似乎正逐渐增多。不过,即使团队鼓励编写文档,注释也常被视为繁琐的杂务。许多开发人员又不知道该怎么写,结果文档质量往往平平。文档不足给软件开发带来了巨大且不必要的拖累。
本章将讨论开发人员不写注释的借口,以及注释为何如此重要。第 13 章将介绍如何写好注释,后续几章则讨论相关问题,例如如何选择变量名、如何借助文档改进系统设计。我希望这些章节能让你相信三件事:好的注释能显著改善软件的整体质量;写好注释并不难;而且,写注释其实可以很有趣,虽然这可能让人难以相信。
当开发人员不写注释时,他们通常会以以下一种或多种借口为自己的行为辩护:
- “好的代码是自解释的。”
- “我没有时间写注释。”
- “注释很容易过时,并会产生误导。”
- “我所看到的注释都是毫无价值的,何必呢?”
下面几个小节将依次讨论这些借口。
12.1 好的代码是自解释的
有人认为,只要代码写得好,逻辑就清楚,不需要注释。这是一个“诱人的谬误”,就像“冰淇淋有益健康”的说法一样,让人很想相信。可惜事实并非如此。编写代码时,确实可以采取一些措施来减少对注释的需求,例如选择好的变量名(请参阅第 14 章)。但仍有大量设计信息无法用代码表达。例如,类接口中只有很小一部分能在代码中形式化地指定,如方法签名。接口的非形式化部分,如每个方法做什么、结果代表什么的高层级描述,只能通过注释说明。还有许多信息也无法用代码表达,例如某个设计决策背后的考量,或在什么情况下适合调用某个方法。
一些开发人员认为,要了解某个方法的作用,只需阅读其代码,这比任何注释都准确。读者确实可能从代码中推断出方法的抽象接口,但这既费时又费力。而且,如果你预期使用者会阅读方法的实现,就会尽量把每个方法写短,以便阅读。方法所做的事情稍微复杂一些,就会被拆成几个较小的方法,最终产生大量浅方法。这也没有真正提高可读性:为了理解顶层方法的行为,读者仍可能需要了解那些较小方法的行为。对于大型系统,要求使用者靠阅读代码来了解系统行为并不现实。
此外,注释是抽象的基础。第 4 章指出,抽象的目的是隐藏复杂性:它是实体的简化视图,保留必要信息,省去可以安全忽略的细节。如果用户必须阅读方法的代码才能使用它,那就没有任何抽象可言:方法的所有复杂性都暴露了出来。没有注释,方法唯一的抽象就是声明,其中给出方法名,以及参数和返回结果的名称与类型。声明缺少太多必要信息,无法独自构成有用的抽象。例如,提取子字符串的方法可能有“起始”和“结束”两个参数,用来表示字符范围。仅凭声明,无法知道结果是否包含结束位置的字符,也不知道起始位置在结束位置之后时会发生什么。注释可以补充调用者所需的信息,在隐藏实现细节的同时提供简化视图。用人类语言(例如英语)写注释也很重要:虽然不如代码精确,表达能力却更强,能给出简单直观的描述。因此,要用抽象隐藏复杂性,注释必不可少。
译者图解声明留下了哪些互不相容的解释?
12.2 我没有时间写注释
人们很容易把写注释放在其他开发任务之后。如果要在添加新功能和为现有功能写注释之间选择,前者似乎更合理。但软件项目几乎总有时间压力,也总有看起来比写注释更优先的事情。如果任由文档的优先级不断降低,最终就不会有文档。
反驳这一借口,可以用第 3.2 节讨论过的投资思维。要有整洁的软件结构,支持长期高效开发,就必须在前期投入额外时间来构建它。好的注释能显著提高可维护性,投入很快就会得到回报。而且,写注释并不需要很多时间。问问自己:假设完全不写注释,相比设计、编译、测试等工作,真正用于键入代码的时间占多少?我不认为这一比例会超过 10%。再假设键入注释所花的时间与键入代码一样多,这应该是一个稳妥的上限。按这些假设,写好注释最多只会增加约 10% 的开发时间,而良好文档带来的收益会很快抵消这点成本。
此外,许多最重要的注释与抽象有关,例如类和方法的顶层文档。第 15 章将说明,这些注释应当是设计过程的一部分,而编写文档本身就是改进整体设计的重要工具。这些注释立即就能带来回报。
12.3 注释很容易过时,并会产生误导
注释有时确实会过时,但在实践中,这未必会成为大问题。保持文档更新不需要付出很大精力。只有代码大幅修改时,文档才需要大幅修改,而改代码花费的时间会更多。第 16 章讨论了如何组织文档,使它在代码修改后尽可能容易更新,关键是避免重复,并将文档放在相应代码附近。代码审查也能有效发现和修正过时的注释。
12.4 我所看到的所有注释都是毫无价值的
在四个借口中,这一个可能最有道理。每个软件开发人员都见过不提供有用信息的注释,而大多数现有文档充其量也只是平平。好在这个问题可以解决。掌握方法后,写出有用的文档并不难。接下来的几章将提供一套思路,帮助你写好文档并持续维护。
12.5 良好注释的好处
前面讨论了反对写注释的理由,希望也已经说明了它们为何站不住脚。现在来看看好注释的益处。注释的核心作用,是记录设计者知道、却无法在代码中表达的信息。 这些信息既包括底层细节,例如导致代码复杂的硬件特殊行为,也包括高层概念,例如类的设计理由。其他开发人员日后修改代码时,借助注释能更快、更准确地完成工作。没有文档,他们就得重新研究或猜测原设计者的想法,不仅费时,一旦理解有误,还可能引入代码缺陷。即使原作者自己修改代码,注释也很有价值:超过几个星期没有接触某段代码,你就会忘记最初设计时的许多细节。
第 2 章介绍了复杂性在软件系统中表现出来的三种方式:
- 变更放大:看似简单的变更需要在许多地方进行代码修改。
- 认知负荷:为了进行修改,开发人员必须掌握大量信息。
- 未知的未知:尚不清楚需要修改哪些代码,或必须考虑哪些信息才能进行这些修改。
好的文档有助于解决后两个问题。它提供修改代码所需的信息,让开发人员可以忽略无关细节,从而减轻认知负荷。文档不足时,开发人员可能要阅读大量代码,才能还原设计者的思路。文档还可以说明系统结构,让开发人员清楚某项变更涉及哪些信息和代码,从而减少“未知的未知”。
第 2 章指出,复杂性的主要来源是依赖性和模糊性。好的文档可以说明依赖关系,补足缺失的信息,从而消除模糊性。
接下来的几章将介绍如何写好文档,以及如何将文档编写融入设计过程,以改进软件设计。
12.6 不同的观点:注释是一种失败
Robert Martin 在《代码整洁之道》中对注释持更消极的看法:
……注释充其量也只是一种必要之恶。如果编程语言有足够的表达力,或者我们善于用这些语言表达意图,就不会那么需要注释,甚至根本不需要。
注释的恰当用途,是弥补我们未能在代码中表达清楚的内容……注释总是代表着失败。我们无法总是找到不靠注释就表达清楚意图的办法,所以不得不写注释,但这并不值得庆贺。
我同意,好的软件设计可以减少对注释的需求,尤其是方法体中的注释。但注释并不代表失败。注释提供的信息与代码很不一样,而且目前还无法用代码表达。代码和注释各有所长,都有重要价值。即使能设法将注释中的信息表达在代码里,也很难说这就是一种改进。
注释的目的之一,就是让人不必阅读代码。例如,开发人员无需阅读整个方法体,只看一段简短的接口注释,就能获得调用该方法所需的全部信息。Martin 的做法正好相反:他主张用代码替换注释。对于方法中的某个代码块,Martin 建议不写注释解释它,而是将它提取为单独的方法(不加注释),用方法名代替说明。这会产生很长的名称,例如 isLeastRelevantMultipleOfNextLargerPrimeFactor。即使用了这么多单词,这样的名称仍然晦涩,提供的信息还不如一段好注释。而且,每次调用这个方法,开发人员都相当于把它的文档重新输入了一遍!
我担心 Martin 的设计哲学会助长一种不好的心态:程序员为了不被视为失败者,刻意避免写注释。这甚至可能让优秀的设计者受到不公正的批评:“你的代码有什么问题,竟然还需要注释?”
好的注释不是失败。它们提升了代码的价值,并在定义抽象、管理系统复杂性方面发挥着基础作用。