小马的世界

读书笔记-软件设计的哲学【12】为什么要写注释?四种借口

2026-06-21 · 13 min read

代码中的文档(in-code documentation)在软件设计中扮演着至关重要的角色。注释不仅能够帮助开发者理解系统并高效工作,其价值还远不止于此。文档对于抽象(abstraction)同样至关重要;如果没有注释,你就无法隐藏复杂性。

最后,如果方法得当,编写注释这一过程本身实际上还能改善系统设计。

反过来说,即使一个软件拥有优秀的设计,如果文档质量很差,其价值也会大打折扣。遗憾的是,并不是所有人都认同这一观点:大量生产环境代码几乎没有任何注释。

许多开发者认为注释只是浪费时间;也有人认可注释的价值,却始终没有真正去写。值得庆幸的是,越来越多的开发团队开始认识到文档的重要性,并且这样的团队似乎正在逐渐增多。然而,即使是在鼓励编写文档的团队里,注释也常常被视为枯燥乏味的苦差事。许多开发者并不知道该如何写注释,因此最终产生的文档质量平庸。不充分的文档会给软件开发带来巨大的、完全没有必要的负担。

本章将讨论开发者用来逃避编写注释的各种借口,以及为什么这些借口站不住脚。

第 13 章将进一步介绍如何写出优秀的注释;

接下来的几章还会讨论相关主题,例如如何选择变量名,以及如何利用文档改进系统设计。

我希望这些章节能够让你相信三件事:

  • 优秀的注释能够显著提高软件整体质量;
  • 写好注释并不困难;
  • (这点也许难以相信)写注释实际上可以是一件有趣的事情。

当开发者不写注释时,他们通常会用以下一种或多种理由来为自己的行为辩护:

  • “好的代码本身就是文档(Good code is self-documenting)。”
  • “我没有时间写注释。”
  • “注释很快就会过时并产生误导。”
  • “我见过的注释都毫无价值,何必费劲去写?”

接下来的几个小节将逐一讨论这些理由。

好代码本身就是文档(Good code is self-documenting)

有些人认为,只要代码写得足够好,它本身就已经清晰得不需要任何注释。这是一个非常诱人的神话,就像“冰淇淋有益健康”这样的传言一样——我们都很愿意相信它!遗憾的是,这并不是真的。

当然,在编写代码时,你确实可以通过一些方法减少对注释的依赖,例如选择优秀的变量名(参见第 14 章)。然而,仍然存在大量设计信息无法直接体现在代码中。

例如,一个类接口中,真正能够形式化表达在代码里的部分其实很少,主要就是方法签名等内容。而接口的非形式化部分,例如:

  • 每个方法到底做什么;
  • 返回结果代表什么含义;

这些只能通过注释来表达。还有很多其他无法直接通过代码描述的信息,例如:

  • 某项设计决策背后的原因;
  • 在什么条件下调用某个方法才是合理的。

有些开发者认为:如果别人想知道一个方法做什么,那么他们应该直接去阅读这个方法的代码,因为那比任何注释都更准确。确实,通过阅读实现代码,读者有可能推导出该方法的抽象接口。但这种做法既耗时又痛苦。更重要的是,如果你假设用户必须阅读方法实现,那么你会倾向于把每个方法写得尽可能短,以方便阅读。一旦某个方法稍微复杂一点,你就会不断把它拆分成更多的小方法。结果就是系统中出现大量浅层方法(shallow methods)。而这实际上并不会让代码更容易理解:为了理解顶层方法的行为,读者仍然不得不继续深入阅读那些被调用的小方法。对于大型系统而言,依靠阅读代码来理解行为根本不现实。

Pony注释:在AI时代,我认为注释是更加节省token,并且能帮助AI轻松理解代码而不用塞入大量上下文的方式,所以在一开始就让AI留下足够多的注释,能够能加帮助AI来写出正确的代码。

此外,注释是抽象(Abstraction)的核心组成部分。回顾第 4 章:抽象的目标是隐藏复杂性。抽象是对一个实体的简化视图:它保留必要信息,同时隐藏那些可以安全忽略的细节。

如果用户必须阅读一个方法的实现代码才能使用它,那么这个方法实际上根本不存在抽象。

因为此时方法的全部复杂性都暴露出来了。

如果没有注释,一个方法唯一的抽象层就是它的声明(Declaration)。而声明通常只包含:

  • 方法名
  • 参数名称
  • 参数类型
  • 返回值类型

这些信息远远不足以构成一个有用的抽象。

我没有时间写注释

人们很容易把写注释放在比其他开发任务更低的优先级。当面临“新增一个功能”与“为现有功能补充文档”之间的选择时,选择前者似乎是合乎逻辑的。然而,软件项目几乎总是处于时间压力之下,也总会有看起来比写注释更重要的事情。因此,如果你允许文档工作不断被降级优先级,最终的结果就是——没有文档。

针对这种借口的反驳,可以借用先前讨论过的“投资思维”。如果你希望拥有一个整洁的软件结构,并且希望它能够在长期内帮助你高效工作,那么你就必须在一开始投入额外时间去构建这种结构。优秀的注释能够显著提升软件的可维护性,因此投入在文档上的时间,很快就能够获得回报。

此外,编写注释本身其实并不会花费太多时间。

不妨问自己一个问题:

在整个开发过程中,真正用于“敲代码”的时间(而不是设计、编译、测试等活动)究竟占多少比例?假设完全不写任何注释,我怀疑这个比例很难超过 10%。

现在再进一步假设:你花在编写注释上的时间,和写代码本身花费的时间一样多。这已经是一个非常保守、甚至偏高的估计。即使在这种情况下,编写优秀注释所增加的开发成本也不会超过整体开发时间的 10%。而高质量文档所带来的收益,通常会很快抵消这部分额外成本。

此外,许多最重要的注释都与抽象有关,例如:

  • 类的顶层文档(Class Documentation)
  • 方法的接口文档(Method Documentation)

这些注释应该被视为设计过程的一部分。编写文档这一行为本身,其实是一种重要的设计工具,它能够帮助改进整体设计质量。从这个意义上说,这类注释几乎能够立刻收回成本。

注释会过时并产生误导

注释有时确实会过时,但在实践中这并不一定是一个严重问题。

保持文档与代码同步并不需要付出巨大的成本。只有当代码发生较大变化时,才需要对文档进行较大修改,而代码修改本身通常比文档更新更耗时。

第 16 章将讨论如何组织文档,使其在代码修改后尽可能容易维护(核心思想是避免重复文档,并让文档尽可能靠近其对应的代码)。

代码评审(Code Review)也是发现和修复过期注释的重要机制。

我见过的注释都毫无价值

在这四种借口中,这大概是最有道理的一种。几乎每个软件工程师都见过毫无价值的注释,而大多数现有文档的质量也只能算勉强及格。幸运的是,这个问题是可以解决的。一旦掌握正确的方法,编写高质量文档其实并不困难。

接下来的几个章节将构建一套完整框架,说明:

  • 如何编写好的文档;
  • 如何长期维护这些文档;
  • 如何让文档持续发挥价值。

优秀注释带来的收益

既然前面已经讨论(并希望已经驳斥)了那些反对写注释的理由,现在让我们来看看高质量注释究竟能带来什么收益。

注释的核心目标,是记录设计者脑海中的信息,而这些信息无法直接体现在代码中。

这些信息既可能是低层细节,例如:

  • 某个硬件设备的特殊行为;
  • 某段复杂代码背后的特殊约束;

也可能是高层概念,例如:

  • 某个类为何要这样设计;
  • 某项设计决策背后的原因。

当未来的开发者需要修改系统时,这些注释能帮助他们更快、更准确地开展工作。

如果没有文档,后来者只能:

  • 自己重新推导设计者当时掌握的知识;
  • 或者靠猜测理解系统。

这不仅需要额外时间,而且很容易出错。如果用户必须阅读方法的实现代码才能使用它,那么实际上就不存在抽象。此时,方法的全部复杂性都暴露给了使用者。如果没有注释,一个方法唯一的抽象层就是它的声明(Declaration),而声明只包含:

  • 方法名;
  • 参数名与参数类型;
  • 返回值类型。

即使是原设计者本人在修改代码时,注释依然很有价值:如果距离上次接触这段代码已经过去几周以上,你往往已经遗忘了许多当初设计时的重要细节。

第 2 章曾经介绍过软件系统中复杂性的三种表现形式:

变更放大(Change amplification)
一个看似简单的修改,却需要在许多地方同时修改代码。

认知负荷(Cognitive load)
为了完成修改,开发者必须先掌握大量信息。

未知的未知(Unknown unknowns)
开发者无法确定哪些代码需要修改,也不知道需要考虑哪些相关信息。

良好的文档能够帮助解决后两类问题。文档通过向开发者提供完成修改所需的信息,并帮助他们忽略无关信息,从而降低认知负荷。如果缺乏足够的文档,开发者可能不得不阅读大量代码,试图重新构建设计者当时脑中的想法。文档同样能够减少“未知的未知”,因为它会澄清系统结构,使开发者能够明确知道:

  • 哪些信息与当前修改有关;
  • 哪些代码与当前修改有关。

第 2 章还指出,复杂性的主要来源是依赖关系(dependencies)晦涩性(obscurity)

优秀的文档能够帮助澄清依赖关系,同时填补信息空白,从而消除晦涩性。接下来的几个章节将介绍如何编写高质量文档。同时还会讨论如何将编写文档融入设计过程,使其反过来帮助改进软件设计本身。

另一种观点:注释是一种失败

在《Clean Code》一书中,Robert Martin(Bob 大叔)对注释持更加消极的看法:

注释充其量只是一种必要之恶。
如果编程语言足够具有表达力,或者我们能够足够熟练地使用这些语言来表达自己的意图,那么我们几乎不需要注释——甚至完全不需要。

注释的正确用途,是弥补我们无法直接通过代码表达意图这一缺陷。
……注释永远是一种失败。
我们必须依赖注释,是因为我们并不总能找到无需注释就能表达思想的方法;但这并不值得庆祝。

我同意:优秀的软件设计确实能够减少对注释的需求(特别是方法内部的实现注释)。

但是,注释并不意味着失败。 注释所提供的信息,与代码所提供的信息本质上不同,而且这些信息在今天仍然无法完全用代码表达。代码和注释各自适合表达不同类型的信息,并且都具有不可替代的重要价值。即使未来某一天,注释中的内容能够完全用代码表示,也很难说那一定会更好。

注释的一个重要目的,就是让人们不必阅读代码。例如:开发者不需要阅读整个方法实现,只需要阅读一段简短的接口注释,就能够获得调用该方法所需的全部信息。

Martin 采取了相反的策略:他主张用代码替代注释。当需要写注释解释某段代码时,他建议把那段代码提取成独立方法(并且不写注释),然后用方法名来代替注释。这样往往会产生类似下面这种名字:

isLeastRelevantMultipleOfNextLargerPrimeFactor

即使把所有这些单词拼在一起,这种名字仍然晦涩难懂,其传递的信息往往还不如一段写得好的注释。而且采用这种方式后,开发者实际上是在每次调用方法时,重新输入一次文档内容。

我担心 Martin 的理念会鼓励一种错误的程序员心态:为了不让自己看起来像是“失败者”,他们会刻意避免写注释。甚至可能导致优秀的设计者受到不公正的批评:

“你的代码到底出了什么问题,为什么还需要写注释?”

高质量的注释不是失败。

恰恰相反:它们能够提升代码的价值;它们在定义抽象方面发挥着基础性作用;它们是控制系统复杂度的重要工具。