读书笔记
前言
关于注释,程序员最讨厌两件事:一是别人不写注释,一是自己写注释。但是,归根结底,注释还是要写的。《软件设计的哲学》中也讲述了一些案例,来表明注释的重要性,以及如何写注释,如何写好注释。
注释分类
注释最重要的作用之一就是定义抽象。代码不适合描述抽象;它的级别太低,它包含实现细节,这些细节在抽象中不应该看到。描述抽象的唯一方法是使用注释。如果你想要呈现良好抽象的代码,则必须用注释记录这些抽象。
大多数注释属于以下类别之一:
- 接口注释:在模块声明(例如类,数据结构,函数或方法)之前的注释块。注释描述模块的接口。对于一个类,注释描述了该类提供的整体抽象。对于方法或函数,注释描述其整体行为,其参数和返回值(如果有),其生成的任何副作用或异常,以及调用者在调用该方法之前必须满足的任何其他要求。
- 数据结构成员:数据结构中字段声明旁边的注释,例如类的实例变量或静态变量。
- 实现注释:方法或函数代码内部的注释,它描述代码在内部的工作方式。
- 跨模块注释:描述跨模块边界的依赖项的注释。
最重要的注释是前两个类别中的注释。每个类都应有一个接口注释,每个类变量应有一个注释,每个方法都应有一个接口注释。
根据注释抽象能力上的区别,我们着重看一下接口注释和实现注释。接口注释 提供了使用类或方法时需要知道的信息,它们定义了抽象。实现注释 描述了类或方法如何在内部工作以实现抽象。
接口注释
类的接口注释给出了该类提供抽象的高级描述。
方法的接口注释既包括用于抽象的高层信息,又包括用于精度的低层细节:
- 注释通常以一两个句子开头,描述调用者感知到的方法的行为。这是更高层次的抽象。
- 注释必须描述每个参数和返回值。这些注释必须非常精确,并且必须描述对参数值的任何约束以及参数之间的依赖关系。
- 如果该方法有任何副作用,则必须在接口注释中记录这些副作用。副作用是该方法的任何结果都会影响系统的未来行为,但不属于结果的一部分。
- 方法的接口注释必须描述该方法可能产生的任何异常。
- 如果在调用某个方法之前必须满足任何前提条件,则必须对其进行描述(也许必须先调用其他方法,对于二进制搜索方法,必须对要搜索的列表进行排序)。尽量减少前提条件是一个好主意,但是任何保留的条件都必须记录在案。
注释的目的是提供开发人员调用该方法所需的所有信息,包括特殊情况的处理方式。开发人员不必为了调用它而阅读方法的主体,并且接口注释不提供有关如何实现该方法的信息,例如它如何扫描其内部数据结构以查找所需的数据。
实现注释:是什么以及为什么,而非是怎么做(what and way, not how)
实现注释是出现在方法内部的注释,以帮助读者了解它们在内部的工作方式。大多数方法是如此简短、简单,以至于它们不需要任何实现注释:有了代码和接口注释,就很容易弄清楚方法的工作原理。
实现注释的主要目的是帮助读者理解代码在做什么(而不是代码如何工作)。一旦读者知道了代码要做什么,通常就很容易理解代码的工作原理。对于简短的方法,代码只做一件事,该问题已在其接口注释中进行了描述,因此不需要实现注释。较长的方法具有多个代码块,这些代码块作为方法的整体任务的一部分执行不同的操作。在每个主要块之前添加注释,以提供对该块的作用的高级(更抽象)描述。
除了描述代码在做什么之外,实现注释还有助于解释原因。如果代码中有些棘手的方面从阅读中看不出来,则应将它们记录下来。例如,如果一个错误修复程序需要添加目的不是很明显的代码,请添加注释以说明为什么需要该代码。对于错误修复,其中有写得很好的错误报告来描述问题,该注释可以引用错误跟踪数据库中的问题,而不是重复其所有详细信息。
对于更长的方法,为一些最重要的局部变量写注释会很有帮助。但是,如果大多数局部变量具有好名字,则不需要文档。如果变量的所有用法在几行之内都是可见的,则通常无需注释即可轻松理解变量的用途。在这种情况下,可以让读者阅读代码来弄清楚变量的含义。但是,如果在大量代码中使用了该变量,则应考虑添加注释以描述该变量。在记录变量时,应关注变量表示的内容,而不是代码中如何对其进行操作。
不写注释的借口 VS. 编写注释的理由
了解完注释的主要职能以及分类,我们回到第一个话题:程序员最讨厌别人不写注释和自己本人去写注释这么两件事。为了不写注释自然也是想了很多借口的,当然我们是可以一一反驳的。
不写注释的借口
-
“好的代码可以自我注释!”
有人认为,如果代码编写得当,那么显而易见,不需要注释。这是一个美味的神话,就像谣言说冰淇淋对您的健康有益:我们真的很想相信!不幸的是,事实并非如此。可以肯定的是,在编写代码时可以做一些事情来减少对注释的需求,例如选择好的变量名。尽管如此,仍有大量设计信息无法用代码表示。例如,只能在代码中正式指定类接口的一小部分,例如其方法的签名。接口的非正式方面,例如对每种方法的作用或其结果含义的高级描述,只能在注释中描述。
一些开发人员认为,如果其他人想知道某个方法的作用,那么他们应该只阅读该方法的代码:这将比任何注释都更准确。读者可能会通过阅读其代码来推断该方法的抽象接口,但这既费时又痛苦。另外,如果在编写代码时期望用户会阅读方法实现,则将尝试使每个方法尽可能短,以便于阅读。如果该方法执行了一些重要操作,则将其分解为几个较小的方法。这将导致大量浅层方法。此外,它并没有真正使代码更易于阅读:为了理解顶层方法的行为,读者可能需要了解嵌套方法的行为。
此外,注释是抽象的基础。回顾第四章,抽象的目的是隐藏复杂性:抽象是实体的简化视图,该实体保留必要的信息,但忽略了可以安全忽略的细节。如果用户必须阅读方法的代码才能使用它,则没有任何抽象:方法的所有复杂性都将暴露出来。没有注释,方法的唯一抽象就是其声明,该声明指定其名称以及其参数和结果的名称和类型。该声明缺少太多基本信息,无法单独提供有用的抽象。注释使我们能够捕获调用者所需的其他信息,从而在隐藏实现细节的同时完成简化的视图。用人类语言(例如英语)写注释也很重要;这使它们不如代码精确,但提供了更多的表达能力,因此我们可以创建简单直观的描述。如果要使用抽象来隐藏复杂性,则注释必不可少。
-
“我没有时间写注释!”
与其他开发任务相比,将注释的优先级降低是很诱人的选择。在添加新功能和给现有功能增加注释之间做抉择的话,选择新功能似乎合乎逻辑。但是,软件项目几乎总是处于时间压力之下,并且总会有比编写注释优先级更高的事情。因此,如果你允许取消对文档注释的优先级,则最终将没有文档和注释。
如果你想要一个干净的软件架构,可以长期有效地工作,那么就必须花一些额外的时间才能创建该架构。好的注释对软件的可维护性有很大的影响,因此花费在它们上面的精力将很快收回成本。此外,撰写注释不需要花费很多时间。扪心自问,假设不包含任何注释,那么你花费了多少开发时间来键入代码(与设计,编译,测试等相对)。我怀疑答案是否超过 10%。现在假设花在输入注释上的时间与输入代码所花费的时间一样多。这应该是一个安全的上限。基于这些假设,撰写好的注释不会增加您的开发时间约 10%。拥有良好注释的好处将迅速抵消这一成本。
注释应作为设计过程的一部分编写,并且编写文档注释的行为是改善整体设计的重要设计工具。
-
“注释过时,然后会造成误导”
注释有时确实会过时,但这实际上并不是主要问题。使文档注释保持最新状态并不需要付出巨大的努力。仅当对代码进行了较大的更改时才需要对文档注释进行大的更改,并且代码更改将比文档注释的更改花费更多的时间。
-
“我所看到所有注释都是毫无价值的”
在这四个借口中,这可能是最有价值的借口。每个软件开发人员都看到没有提供有用信息的注释,并且大多数现有文档注释基本上都是这样。幸运的是,这个问题是可以解决的。一旦知道了如何编写可靠的文档注释,解决这一问题并不难。
编写注释的理由
注释背后的总体思想是捕获设计者所想但不能在代码中表示的信息。这些信息从低级详细信息(例如,激发特殊代码的硬件怪癖)到高级概念(例如,类的基本原理)。当其他开发人员稍后进行修改时,这些注释将使他们能够更快,更准确地工作。没有文档注释,未来的开发人员将不得不重新编写或猜测开发人员的原始知识。这将花费额外的时间,并且如果新开发者误解了原始设计者的意图,则存在错误的风险。
第2章介绍了在软件系统中表现出复杂性的三种方式:
- 变更放大:看似简单的变更需要在许多地方进行代码修改。
- 认知负荷:为了进行更改,开发人员必须积累大量信息。
- 未知的未知:尚不清楚需要修改哪些代码,或必须考虑哪些信息才能进行这些修改。
好的文档注释可以帮助解决最后两个问题。注释可以减轻开发人员的认知负担,不仅可以为其提供进行更改所需的信息,也可以使他们忽略一些不相关的信息。如果没有足够的文档注释,开发人员可能必须阅读大量代码才能重构设计人员的想法。文档还可以通过阐明系统的结构来减少未知的未知数,从而可以清楚地了解自己要改动代码的相关信息。
第 2 章指出,导致复杂性的主要原因是依赖性和模糊性。好的文档注释可以阐明依赖关系,并且可以填补空白以消除模糊性。
编写注释的建议
1、注释应该描述代码中不那么直观的内容
编写注释的原因是,使用编程语言编写的语句无法捕获编写代码时开发人员想到的所有重要信息。注释记录了这些信息,以便后来的开发人员可以轻松地理解和修改代码。注释的指导原则是,注释应描述代码中不是那么显而易见的内容。
从代码中看不到很多东西。有时,底层细节并不直观。
写注释的最重要原因之一是描述抽象,其中包括许多从代码中看不到的信息。抽象的思想是提供一种思考问题的简单方法,但是代码是如此详细,以至于仅通过阅读代码很难看到抽象。注释可以提供一个更简单,更高级的视图(“调用此方法后,网络流量将被限制为每秒 maxBandwidth 字节”)。
好的注释通常以与代码不同的详细程度来解释事物,在某些情况下,注释会更详细,而在某些情况下,代码则不那么详细(更抽象)。
2、选择注释约定俗称的格式(公约)
编写注释的第一步是确定注释的公约,例如您要注释的内容和注释的格式。如果你正在使用某些语言的 IDE 进行编程,例如 Java 的 Javadoc,C ++的 Doxygen 或 Go 的 godoc,请遵循工具的约定。这些约定并非是完美的,但这些工具可提供足够的好处来弥补这一缺点。如果在没有现有约定可遵循的环境中进行编程,请尝试从其他类似的语言或项目中采用这些约定;这将使其他开发人员更容易理解和遵守这些公约。
公约有两个目的。首先,它们可以确保一致性,这使得注释更易于阅读和理解。其次,它们有助于确保你的的确确是写了注释,如果你不清楚这些约定,那很有可能就不写了。
3、不要重复编写
许多注释并不是特别有用。最常见的原因是注释重复了代码:可以轻松地从注释旁边的代码中推断出注释中的所有信息。如果注释旁边的代码中的信息已经很明显,则注释则显得画蛇添足。
编写注释后,请自问一下:从未看过代码的人能否仅通过查看注释旁边的代码来编写注释?如果答案是肯定的,则注释并不会使代码更易于理解。这也是为什么有人会认为这样的注释毫无价值。
编写良好注释的第一步是在注释中使用与所描述实体名称不同的词。为注释选择的词句,要提供有关实体含义的更多信息,而不仅仅是重复其名称。
4、低层级的注释可以提高精确度
注释通过提供不同详细程度的信息来增强代码。一些注释提供了比代码更低层次,更详细的信息。这些注释通过阐明代码的确切含义来增加精确度。其他注释提供了比代码更高的层级,更抽象的信息。这些注释更直观,可以一眼便能理解,比如代码背后的推理,或者更简单,更抽象的代码思考方式。与代码处于同一级别的注释可能会重复该代码。
在注释变量声明(例如类实例变量,方法参数和返回值)时,精准描述最有用的。变量声明中的名称和类型通常不是很精确,注释可以填写缺少的详细信息,例如:
- 此变量的单位是什么?
- 边界条件是包容性还是排他性?
- 如果允许使用空值,则意味着什么?
- 如果变量引用了最终必须释放或关闭的资源,那么谁负责释放或关闭改资源?
- 是否存在某些对于变量始终不变的属性(不变量),比如“此列表始终包含至少一个条目”?
5、高层级的注释可以增加直观性
注释可以增强代码的第二种方法是提供直观性。这些注释是在比代码更高的层级上编写的。它们忽略了细节,并帮助读者理解了代码的整体意图和结构。此方法通常用于方法内部的注释以及接口注释。
高层级的注释比低级别的注释更难编写,因为你必须以不同的方式考虑代码。问问自己:这段代码要做什么?您能以何种最简单方式来解释代码中的所有内容?这段代码最重要的是什么?
工程师往往非常注重细节。我们喜欢细节,善于管理其中的许多细节;这对于成为一名优秀的工程师至关重要。但是,优秀的软件设计师也可以从细节退后一步,从更高层次考虑系统。这意味着要确定系统的哪些方面最重要,并且能够忽略底层细节,仅根据系统的最基本特征来考虑系统。这是抽象的本质(找到一种思考复杂实体的简单方法),这也是编写高级注释时必须执行的操作。一个好的高层注释表达了一个或几个简单的想法,这些想法提供了一个概念框架,例如“附加到现有的 RPC”。使用该框架,可以很容易地看到特定的代码语句与总体目标之间的关系。
6、先写注释
迟到的注释不是好注释。即使你有自律性回去写注释(不要欺骗你自己:你八成没有),注释也不会很好。在这个过程的这个时候,你已经在精神上离开了。在你的脑海中,这段代码已经完成了;你急于开始下一个项目。你知道写注释是正确的事情,但它没有乐趣。你只想尽快度过难关。因此,您可以快速地浏览代码,添加足够的注释以使其看起来令人满意。到目前为止,您已经有一段时间没有设计代码了,所以您对设计过程的记忆变得模糊了。您在编写注释时查看代码,因此注释重复了代码。即使您试图重构代码中不明显的设计思想,也会有您不记得的事情。因此,这些注释忽略了他们应该描述的一些最重要的事情。
我使用一种不同的方法来编写注释,在开始时就写注释:
- 对于新类。我首先编写类接口注释。
- 接下来,我为最重要的公共方法编写接口注释和签名,但将方法主题保留为空。
- 我对这些注释进行了迭代,知道基本结构感觉正确为止。
- 在这一点上,我为类中最重要的类示例变量编写了声明和注释。
- 最后,我填写方法的主体,并根据需要添加实现注释。
- 在编写方法主体时,我通常会发现需要其他方法和实例变量。对于每个新方法,我在方法主体之前编写接口注释。例如变量,我在编写变量声明的同时填写了注释。
代码完成后,注释也将完成。从来没有积压的书面注释。
注释先行具有三个好处。首先,它会产生更好的注释。如果你在设计时写注释,那么关键的设计问题将在你的脑海中浮现,因此很容易记录下来。最好在每个方法的主体之前编写接口注释,这样你就可以专注于方法的抽象和接口,而不会因其实现而分心。在编码和测试过程中,您会注意到并修复注释问题。结果,注释在开发过程中得到了改善。
注释是一种设计工具。 第二,也是最重要的一点,在开始的时候就编写注释,其好处是可以改善系统设计。注释提供了捕获抽象的唯一方法,好的抽象是好的系统设计的基础。如果在一开始就写了描述抽象的注释,则可以在编写实现代码之前对其进行检查和调整。要写一个好的注释,你必须确定一个变量或一段代码的本质:这件事最重要的方面是什么?在设计过程的早期进行此操作很重要;否则,您只是在写代码而已。
描述方法或变量的注释应该简单而完整。如果你发现很难写这样的注释,则表明你所描述内容的设计可能存在问题。
当然,如果注释完整而清晰,那么它们仅是复杂度的良好指标。如果编写的方法接口注释未提供调用该方法所需的全部信息,或者编写的注释过于神秘以至于难以理解,则该注释不能很好地衡量该方法的深度。
早期的注释往往都是很有趣的。 尽早编写注释的第三个也是最后一个好处是,它使编写注释更加有趣。对我来说,编程中最有趣的部分之一是新类的早期设计阶段,在那里,我将充实该类的抽象和结构。我的大部分注释都是在此阶段编写的,这些注释是我记录和测试设计决策质量的方式。我正在寻找可以用最少的词来完整而清晰地表达的设计。注释越简单,我对设计的感觉就越好,因此找到简单的注释是一种自豪感。
7、维护注释
将注释保持在代码附近。更改现有代码时,这些改动很有可能会使某些现有注释无效。修改代码时,很容易忘记更新注释,从而导致注释不再准确。不准确的注释使读者感到沮丧,如果注释太多,读者就会开始不信任所有注释。幸运的是,只要有一点纪律和一些指导规则,就可以在不付出巨大努力的情况下使注释保持最新。
确保注释更新的最佳方法是将注释放置在它们描述的代码附近,以便开发人员在更改代码时可以看到它们。注释离其关联的代码越远,正确更新的可能性就越小。例如,方法接口注释的最佳位置是在代码文件中,紧靠该方法主体的位置。对方法的任何更改都将涉及此代码,因此开发人员很可能会看到接口注释,并在需要时进行更新。
在编写实现注释时,不要将整个方法的所有注释放在方法的顶部。展开它们,将每个注释压缩到最小的范围,其中包括该注释所引用的所有代码。例如,如果一种方法具有三个主要阶段,则不要在方法的顶部写一个详细描述所有阶段的注释。而是为每个阶段编写一个单独的注释,并将该注释放置在该阶段的第一行代码的正上方。另一方面,在描述总体策略的方法实现的顶部添加注释也可能会有所帮助。
通常,注释离描述的代码越远,注释应该越抽象(这减少了注释因代码更改而无效的可能性)。
避免重复。保持注释最新的第二种方法是避免重复。如果注释文档重复,那么开发人员将很难找到并更新所有相关副本。如果代码中有多个地方受某个特定决定的影响,请不要在所有这些地方重复注释文档。相反,找到放置注释文档最显眼的位置。例如,假设存在与变量相关的棘手行为,这会影响使用变量的几个不同位置。你就可以在该变量声明旁边的注释中记录该行为。这是很合理的,开发人员可能会确认他们是否在理解使用该变量的代码时遇到问题(而去阅读注释)。
如果没有一个“明显的”地方来放置特定的文档,可以让开发人员可以找到它,那么创建一个 designNotes 文件,或者,选择可用位置最好的一个,把注释文档放在那里。
不要在另一个模块中记录一个模块的设计决策。例如,不要在方法调用前添加注释,以解释被调用方法中发生的情况。如果读者想知道,他们应该查看该方法的接口注释。好的开发工具通常会自动提供此信息,例如,如果您选择了方法的名称或将鼠标悬停在该方法的名称上,则将显示该方法的接口注释。
如果信息已经在程序之外的某个地方记录了,不要在程序内部重复记录;只需参考外部文档。
**检查差异。**确保注释文档保持最新状态的一种好方法是,在将更改提交到修订控制系统之前需要花费几分钟,以扫描该提交的所有更改。确保文档中正确反映了每个更改。这些预先提交的扫描还将检测其他一些问题,例如意外地将调试代码留在系统中或无法修复 TODO 项目。
总结
注释对于软件设计和软件项目都是相当重要的,它是代码的一部分,并为解释代码,方便修改和拓展方面有着巨大的作用。所以在最初进行软件设计的时候我们就应该动手写注释,把软件的功能抽象出来,不仅降低了整个软件的复杂度,对于后续的编码工作也会更加有益。
链接
- 《软件设计的哲学》中文翻译:github.com/inkydragon/…
- 我的笔记:github.com/lq920320/ba…