干净的代码注释与类文档


83

我正在与新同事讨论有关评论的问题。我们俩都喜欢Clean Code,并且我完全可以避免内联代码注释,并且应该使用类和方法名称来表示它们的作用,这一点我完全满意。

但是,我非常喜欢添加小类摘要,以尝试解释类的目的和实际代表的内容,主要是为了使其易于维护单一职责原则模式。我也习惯在方法中添加单行摘要,以说明该方法应该执行的操作。一个典型的例子是简单的方法

public Product GetById(int productId) {...}

我添加以下方法摘要

/// <summary>
/// Retrieves a product by its id, returns null if no product was found.
/// </summary

我认为应该记录该方法返回null的事实。想要调用方法的开发人员不必打开我的代码即可查看该方法是否返回null或引发异常。有时它是接口的一部分,因此开发人员甚至都不知道正在运行哪个基础代码?

但是,我的同事认为,这类注释是“ 代码异味 ”,“注释始终是失败的”(Robert C. Martin)。

有没有一种方法可以表达和交流这些类型的知识而无需添加评论?由于我是Robert C. Martin的忠实粉丝,所以我有点困惑。摘要与注释相同,因此总是失败吗?

这不是有关在线注释的问题。


38
罗伯特·马丁说:“评论总是失败”?好吧,那么他是个边缘极端主义者,应该带一点盐吃。(是的,我知道他是出于修辞目的而这样写的,以便传达他的信息。我的意思是,你也应该如此。)
Kilian Foth,2015年

18
鲍勃叔叔的书应该随附一公斤的盐袋……
AK_15年

6
如果您关注的是罗伯特·马丁(Robert Martin),则空案例的文档应该是测试。也就是说,您应该进行测试以显示在这种情况下该方法可以返回null。或者,由于这是Java,因此@Nullable注释也比注释更好。
Martin Epsz 2015年

15
@Bjorn我拥有“清洁代码”的副本,并且已经阅读了涵盖多次的内容。是的,鲍伯叔叔更喜欢代码能够自我记录,但是在书中自己的代码中有多个注释示例。关键是,如果您觉得自己不得不写评论,请尽力更改代码而不是添加评论,但不要完全禁止评论(甚至是内嵌评论)。

6
该方法应称为TryGetById,并应删除注释。
usr 2015年

Answers:


116

正如其他人所说,API文档注释和嵌入式注释之间存在区别。从我的角度来看,主要区别在于,在代码旁边读取了嵌入式注释,而在您注释的内容的签名旁边读取了文档注释。

鉴于此,我们可以应用相同的DRY原理。评论是否与签名相同?让我们看一下您的示例:

通过ID检索产品

这一部分只是重复我们已经在名称GetById加上返回类型中看到的内容Product。这也提出了一个问题,即“获取”和“获取”之间的区别是什么,以及在该区别上带有什么代码与注释。因此,这是不必要的,并且有些混乱。如果有的话,它会妨碍实际有用的第二部分注释:

如果找不到产品,则返回null。

啊! 我们绝对不能仅仅从签名中就知道这一点,并且提供了有用的信息。


现在,进一步采取此步骤。当人们谈论代码的味道时,问题不在于代码本身是否需要注释,而在于注释是否表明可以更好地编写代码以表达注释中的信息。这就是“代码气味”的含义-并不意味着“不要这样做!”,它的意思是“如果您这样做,可能表明存在问题”。

因此,如果您的同事告诉您有关null的注释是一种代码味道,您应该简单地问他们:“好吧,那我该如何表达呢?” 如果他们有可行的答案,那么您已经学到了一些东西。如果没有,这可能会杀死他们的投诉。


关于这种特殊情况,通常众所周知,空问题是一个困难的问题。有一个原因是代码库中堆满了保护子句,为什么空检查是代码契约的流行前提,为什么空的存在被称为“十亿美元的错误”。没有太多可行的选择。但是,在C#中发现的一种流行的Try...约定是:

public bool TryGetById(int productId, out Product product);

在其他语言中,使用一种类型(通常称为OptionalMaybe)来表示可能存在或可能不存在的结果可能是惯用的:

public Optional<Product> GetById(int productId);

因此,在某种程度上,这种反评论立场使我们进入了某种位置:我们至少考虑过此评论是否代表一种气味,以及对我们而言可能存在哪些替代方法。

我们是否应该真正喜欢这些在原始签名是一个整体的其他辩论,但我们至少有选择的通过代码,而不是表达意见时,没有发现产品会发生什么。您应该与您的同事讨论他们认为哪种选择更好以及为什么选择,并希望能帮助他们超越关于评论的笼统教条。


9
或-的- Linq等效项Try...,如果子句将导致空结果...OrDefault,则返回default(T)
Bart van Nierop 2015年

4
非常感谢您对内联代码注释和文档注释之间的区别,以及给出的示例:)
Rachel

2
函数的可能返回值应通过其签名来证明。该TryGetValue模式是在C#这样做的合理方式,但大多数函数式语言都代表缺失值的更好的方法。在此处阅读更多内容
AlexFoxGill 2015年

2
@BenAaronson:如果希望拥有一个可以支持协方差的通用接口,则可以将其T TryGetValue(params, ref bool success)用于任何类型T,或者T TryGetValue(params)将null表示失败,用于类受限的类型T,但是TryGetXX返回的模式bool与协方差不兼容。
超级猫

6
在Java 8中,您可以返回Optional<Product>来指示该方法可能没有返回Product。
Wim Deblauwe 2015年

102

罗伯特·C·马丁(Robert C. Martin)的引用是出于上下文。这是带有更多上下文的报价:

没有什么比放置适当的评论那么有用了。没有什么比琐碎的教条式注释更容易使模块混乱了。没有什么比散布谎言和错误信息的陈旧粗鲁的评论造成的破坏那么严重了。

评论不像迅达的名单。他们不是“纯粹的好人”。实际上,评论充其量是必不可少的。如果我们的编程语言具有足够的表达能力,或者如果我们有能力巧妙地运用这​​些语言来表达我们的意图,那么我们将不需要太多注释,也许根本就不需要注释。

注释的正确用法是为了弥补我们未能在代码中表达自己的意愿。请注意,我使用了失败一词。我是真的 评论总是失败。我们必须拥有它们,因为我们不能总是想出没有它们的表达方式,但是它们的使用并不是值得庆祝的原因。

因此,当您处于需要编写注释的位置时,请仔细考虑一下,看看是否有某种方法可以翻转表格并用代码表达自己。每次用代码表达自己时,都应该轻拍一下。每次发表评论时,您都应该做个鬼脸,并感到自己表达能力的失败。

从此处复制,但是原始引用来自“ 清洁代码:敏捷软件技巧手册”

如何将此报价简化为“注释始终是失败”,这是一个很好的示例,说明了某些人如何将明智的报价从上下文中提取出来并将其转变为愚蠢的教条。


API文档(例如javadoc)应该记录API,以便用户无需阅读源代码即可使用它。因此,在这种情况下,文档说明该方法的作用。现在您可以说“通过其ID检索产品”是多余的,因为它已经由方法名称指示,但是null可能返回的信息对于记录文档绝对重要,因为这一点丝毫不明显。

如果要避免注释的必要性,则必须null通过使API更显式来消除潜在的问题(将用作有效的返回值)。例如,您可以返回某种Option<Product>类型,因此类型签名本身可以清楚地传达在未找到产品的情况下将返回的内容。

但是无论如何,仅通过方法名称和类型签名来完全记录API是不现实的。将文档注释用于用户应知道的任何其他非显而易见的信息。从DateTime.AddMonths()BCL中获取API文档:

AddMonths方法考虑leap年和一个月中的天数来计算所得的月份和年份,然后调整所得的DateTime对象的日期部分。如果结果日不是结果月中的有效日,则使用结果月中的最后一个有效日。例如,3月31日+ 1个月= 4月30日。结果DateTime对象的时间部分与此实例相同。

您无法仅使用方法名称和签名来表达这一点!当然,您的班级文档可能不需要此级别的详细信息,仅是示例。


内联注释也不错。

不好的评论是不好的。例如,仅说明可以从代码中轻松看到的注释的示例,经典示例为:

// increment x by one
x++;

注释解释了一些可以通过重命名变量或方法或通过重组代码来使事情变得清晰的东西,这是代码的味道:

// data1 is the collection of tasks which failed during execution
var data1 = getData1();

这些是马丁批评的那种评论。该注释是未能编写清晰代码的征兆-在这种情况下,对变量和方法使用不言自明的名称。注释本身当然不是问题,问题在于我们需要注释才能理解代码。

但是应该使用注释来解释所有在代码中不明显的内容,例如为什么以某种非显而易见的方式编写代码:

// need to reset foo before calling bar due to a bug in the foo component.
foo.reset()
foo.bar();

注释解释了一个过于复杂的代码片段,这也是一种味道,但解决方法不是禁止注释,解决方法是修复代码!实际上,确实发生了混乱的代码(希望只是暂时的直到重构),但没有普通的开发人员第一次编写完美的干净代码。当卷积的代码发生时,写一个注释解释它的作用比写注释要好得多。此注释还将使以后重构更加容易。

有时代码不可避免地很复杂。它可能是复杂的算法,或者出于性能原因可能是牺牲清晰度的代码。再次需要注释。


13
还有在您的代码处理,只是一种情况的情况复杂的,并没有简单的代码可以处理它。
gnasher729

6
好点,漱口。当您必须优化某些代码以提高性能时,这似乎经常发生。
JacquesB 2015年

9
x++如果是诸如“将x递增1,如果是UINT32_MAX,则换行”之类的东西,即使是注释也可能是不错的;任何了解语言规范的人都知道增加a uint32_t将自动换行,但是如果没有注释,则可能不知道这种自动换行是否是所实现算法的预期部分。
超级猫

5
@ l0b0:希望你在开玩笑!
JacquesB 2015年

9
@ l0b0没有诸如临时代码之类的东西。该代码永远不会被重构,因为企业对结果感到满意,并且不会批准资金来修复它。从现在开始的五年后,一些初级开发人员将看到此代码,因为您已将其替换为Bugtrocity v9,所以您甚至都不再使用WizBug 4.0,因此“ Bug123”对他而言毫无意义。现在,他认为这应该是永久性代码,并在整个职业生涯中成为一名糟糕的开发人员。想想孩子们。不要写临时代码。
corsiKa 2015年

36

注释代码记录代码之间是有区别的。

  • 以后需要注释来维护代码,即更改代码本身。

    评论确实确实被认为是有问题的。极端点是说自己总是表明一个问题,无论是在你的代码(代码太难理解的)或语言(language无法有足够的表现力之内;例如,事实证明该方法不会返回null可以表示通过C#中的代码合同进行,但是无法通过PHP中的代码来表达)。

  • 需要文档才能使用您开发的对象(类,接口)。目标受众是不同的:我们在这里谈论的不是维护您的代码并对其进行更改的人员,而是几乎不需要使用它的人员。

    因为代码足够清晰,所以删除文档是很疯狂的,因为此处的文档专门用于使类和接口的使用成为可能,而不必阅读数千行代码


是的,但至少马丁的观点是,在现代开发实践中,测试是文档,而不是代码本身。假设代码正在使用BDD风格的测试系统(例如specflow)进行测试,则测试本身就是方法行为的直接可读描述(“当使用有效产品ID调用GetById时,给出了产品数据库,当使用无效的产品ID调用GetById时,将返回适当的Product对象,然后返回null”或类似的东西。
Jules

13

好吧,您的同事似乎在读书,接受他们所说的话,并运用他学到的东西而没有思考,也没有考虑任何上下文。

您对函数的功能的评论应该这样,您可以丢弃实现代码,阅读该函数的评论,并且可以编写实现的替换,而不会出错。

如果评论没有告诉我是否引发了异常或是否返回nil,则无法执行此操作。此外,如果注释没有告诉是否抛出异常或是否返回nil,则无论在何处调用该函数,都必须确保代码是否正确运行(无论抛出异常还是返回nil)。

所以你的同事是完全错误的。继续阅读所有书籍,但要自己考虑。

PS。我看到您的行“有时它是接口的一部分,所以您甚至都不知道正在运行的代码。”即使使用虚函数,您也不知道正在运行的代码。更糟糕的是,如果您编写了一个抽象类,甚至没有任何代码!因此,如果您有一个带有抽象函数的抽象类,则添加到抽象函数中的注释是具体类的实现者必须指导它们的唯一事情。这些注释也可能是唯一可以指导该类用户的内容,例如,如果您拥有的只是一个抽象类和一个返回具体实现的工厂,但是您从未看到该实现的任何源代码。(当然,我不应该查看一种实现的源代码)。


我十年没有评论代码了。评论很blo肿,很垃圾。这些天没有人评论代码。我们专注于格式正确且命名良好的代码,小型模块,去耦等。这使您的代码可读而不是注释。测试可确保如果丢弃代码,则不会出错,不会注释。测试告诉您如何使用编写的代码,如何调用它们以及为什么它们首先存在。您的学校太老了,您需要学习有关测试和清除代码的知识,我的朋友。
PositiveGuy17年

12

有两种类型的注释可供考虑-那些对代码人员可见的注释和用于生成文档的注释。

Bob叔叔所指的注释类型是只有使用代码的人才能看到的注释类型。他提倡的是DRY的一种形式。对于正在查看源代码的人员,源代码应该是他们所需的文档。即使在人们可以访问源代码的情况下,注释也不总是不好的。有时,算法很复杂,或者您需要捕获为什么采用一种非显而易见的方法,以便其他人在尝试修复错误或添加新功能时最终不会破坏您的代码。

您描述的注释是API文档。这些是使用您的实现的用户可以看到的,但是可能无法访问您的源代码。即使他们确实有权访问您的源代码,他们也可能正在其他模块上工作,而不在查看您的源代码。这些人会发现在编写代码时在IDE中提供此文档很有用。


老实说,我从来没有想到过将DRY应用于代码+注释,但这是完全合理的。有点像@JacquesB的答案中的“增量X”示例。

7

评论的价值以其提供的信息的价值减去阅读和/或忽略它所需的精力来衡量。所以如果我们分析评论

/// <summary>
/// Retrieves a product by its id, returns null if no product was found.
/// </summary>

对于价值和成本,我们看到三件事:

  1. Retrieves a product by its id重复函数名称说的话,因此代价就是没有价值。应该删除它。

  2. returns null if no product was found是非常有价值的信息 这可能会减少其他编码人员必须查看功能实现的时间。我敢肯定,它节省的阅读成本要比它自己介绍的阅读成本高。它应该留下来。

  3. 线

    /// <summary>
    /// </summary>
    

    不携带任何信息。对于评论的读者来说,它们是纯成本。如果您的文档生成器需要它们,则可能是合理的,但在这种情况下,您可能应该考虑使用其他文档生成器。

    这就是为什么使用文档生成器是一个有争议的想法的原因:它们通常需要大量其他注释,这些注释不带任何信息或重复明显的内容,仅是为了完善文档。


我没有在其他任何答案中找到的观察结果:

甚至评论说,是不是有必要了解/使用的代码可以是非常有价值的。这是一个这样的例子:

//XXX: The obvious way to do this would have been ...
//     However, since we need this functionality primarily for ...
//     doing this the non-obvious way of ...
//     gives us the advantage of ...

可能很多文本,完全不需要理解/使用代码。但是,它解释了代码看起来像以前一样的原因。这将使人们停止查看代码,不知道为什么它没有采用明显的方式,并开始重构代码,直到他们意识到为什么首先编写这样的代码。即使读者足够聪明,不能直接跳转到重构,他们仍然需要弄清楚为什么代码看起来像以前一样,然后才意识到最好保持现状。该评论实际上可以节省工作时间。因此,价值高于成本。

同样,注释可以传达代码的意图,而不仅仅是代码的工作方式。他们可以描绘通常在代码本身的细节中迷失的全局。因此,您赞成班级评论是正确的。如果他们解释类的意图,类与其他类的相互作用,如何使用等,那么我会最看重它们。


2
哇-更改文档生成器,因为它需要几行额外的html行才能进行解析?不要吧。
corsiKa 2015年

2
@corsiKa YMMV,但是我更喜欢一个文档生成器,它将注释的成本降至最低。当然,我也宁愿阅读写得很好的头文件,也不愿阅读与实际代码不同步的doxygen文档。但是,正如我所说,YMMV。
cmaster

4
即使方法的名称很好地描述了其目的,使用自然语言重复该目的也可以使阅读它的人更容易将其与随后的所有警告联系起来。重述名称中描述的内容将足够简短,即使值很小,成本也很低。因此,我不同意您文章的第一部分。但是,第二部分为+1。被评估和拒绝的替代方法的文档可能非常有价值,但是鉴于应得到的重视,此类信息很少。
超级猫

GetById提出了一个问题,什么是ID,以及从何处获得什么。文档注释应允许开发环境显示这些问题的答案。除非在模块文档注释的其他地方进行了说明,否则它还是一个可以告诉您为什么仍会获得ID的地方。
海德

注释,干净的代码(自我描述),TDD(经常获得反馈并经常获得有关设计的反馈)和测试(给您信心和文档行为)规则!测试人员测试。这里没有人在谈论这个。醒来
PositiveGuy

4

未注释的代码是错误的代码。一个普遍的(如果不是普遍的)神话是,代码的读取方式与英语类似。它必须被解释,除了最琐碎的代码之外,任何其他代码都需要花费时间和精力。另外,每个人在阅读和编写任何给定语言方面都有不同程度的能力。作者与读者的编码风格和能力之间的差异是准确解释的强大障碍。您可以从代码的实现中得出作者的意图,这也是一个神话。以我的经验,添加额外的评论很少是错误的。

罗伯特·马丁(Robert Martin)等。将其视为“难闻的气味”,因为可能是代码已更改而注释未更改。我说这是一件好事(就像在家用气体中添加“难闻的气味”以提醒用户泄漏一样)。阅读注释为您解释实际代码提供了一个有用的起点。如果它们匹配,那么您将对代码有更大的信心。如果它们不同,则您已检测到警告气味,需要进一步调查。解决“难闻的气味”的方法不是去除异味,而是密封泄漏。


2
假设您是一位聪明的作者,而文字是为了让聪明的读者受益;您使用常识来划定界限。显然,就目前而言,该示例是愚蠢的,但这并不是说没有充分的理由来说明为什么此时代码包含i ++。
文斯·奥沙利文

8
i++; // Adjust for leap years.
文斯·奥沙利文

5
“ Robert Martin等人将其视为“难闻的气味”,因为可能是代码已更改而注释未更改。” 那只是气味的一部分。最糟糕的气味来自于这样的想法,即程序员决定不尝试以更具描述性的方式编写代码,而是选择在注释中“密封泄漏”。他的断言是,应该在代码本身(或类似内容)中使用“ adjustForLeapYears()”方法,而不是打“ //调整leap年”注释。该文档采用行使the年逻辑的测试形式。
埃里克·金

3
我会考虑添加一个方法调用,以用注释过大的代码替换一行代码,尤其是因为该方法的名称实际上实际上只是注释标记了一段代码,而不能保证更好的文档准确性。(如果该行发生在两个或多个位置,那么引入方法调用当然是正确的解决方案。)
文斯·奥沙利文

1
@Jay原因是使您的抽象明确(例如通过引入方法)规则。不这样做是因为例外,因为您可能会得到只有一行的方法。让我解释一下真正的任意规则:“使用编程语言的结构(通常是方法和类)引入抽象,除非该抽象的实现可以表示为一行代码,在这种情况下,通过添加自然语言来指定抽象一条评论。”
Eric

3

在某些语言中(例如F#),整个注释/文档实际上可以在方法签名中表示。这是因为在F#中,除非明确允许,否则null通常不是允许的值。

F#(以及其他几种功能语言)的共同点是,您使用的选项类型Option<T>可以是None或,而不是null Some(T)。然后,该语言会理解这一点,并在您尝试使用这两种情况时迫使您(或者如果您不这样做则警告您)。

因此,例如在F#中,您可以拥有一个如下所示的签名

val GetProductById: int -> Option<Product>

然后这将是一个接受一个参数(int)然后返回乘积或值None的函数。

然后您可以像这样使用它

let product = GetProduct 42
match product with
| None -> printfn "No product found!"
| Some p -> DoThingWithProduct p

如果两种情况都不匹配,则会收到编译器警告。因此,不可能在那里获得null引用异常(除非您当然忽略编译器警告),并且仅通过查看函数签名即可知道所需的一切。

当然,这要求您的语言是以这种方式设计的-许多普通语言(例如C#,Java,C ++等)却没有。因此,在您当前的情况下,这可能对您没有帮助。但是,很高兴知道有多种语言可以让您以静态类型的方式表达此类信息,而无需诉诸注释等。:)


1

这里有一些很好的答案,我不想重复他们说的话。但是,让我添加一些评论。(没有双关语。)

聪明的人有很多关于软件开发和许多其他主题的陈述,我认为这是非常好的建议,但在上下文中理解时,却比从上下文中脱颖而出或在不适当的情况下应用或愚蠢的人愚蠢。荒谬的极端。

代码应该自我记录的想法就是这样一个极好的想法。但是在现实生活中,这种想法的实用性受到限制。

值得注意的是,该语言可能没有提供功能来清晰,简洁地记录需要记录的内容。随着计算机语言的改进,这已成为越来越少的问题。但是我不认为它已经完全消失了。早在我写汇编程序时,添加诸如“总价=非应税项目的价格加应税项目的价格加应税项目的价格加应税项目的价格*税率”这样的注释是很有意义的。在任何给定时刻,寄存器中的确切内容不一定是显而易见的。采取了许多步骤来进行简单的操作。等等,但是,如果您使用的是现代语言,那么这样的注释将只是重新声明一行代码。

当我看到诸如“ x = x + 7; //将7加到x”之类的评论时,我总是很烦。就像,哇,谢谢,如果我忘了加号意味着什么可能很有帮助。我可能真正感到困惑的地方是知道“ x”是什么,或者为什么在这个特定时间需要将“ 7”添加到它。通过给“ x”赋予更有意义的名称并使用符号常量而不是7,可以使此代码自动记录文档。例如,如果您编写了“ total_price = total_price + MEMBERSHIP_FEE;”,则可能根本不需要注释。 。

听起来不错,一个函数名称应该告诉您确切的功能,以便任何附加的注释都是多余的。我曾经写过一个函数,该函数检查项目编号是否在我们的数据库Item表中,并返回true或false,并称其为“ ValidateItemNumber”,这让我记忆犹新。看起来像一个得体的名字。然后其他人出现并修改了该功能,还为商品创建了订单并更新了数据库,但从未更改名称。现在这个名字很让人误解。听起来它做了一件小事情,但实际上它做得更多。后来有人甚至拿出了有关验证商品编号的部分,并在其他地方做了,但是仍然没有更改名称。因此,即使该功能现在与验证商品编号无关,

但是实际上,如果没有组成函数的代码,函数名称通常不可能完全描述函数的功能。这个名称是否会告诉我们确切地对所有参数执行哪些验证?在异常情况下会发生什么?阐明所有可能的歧义?在某些时候,名称会变得很长,以至于变得混乱。我会接受String BuildFullName(String firstname,String lastname)作为体面的函数签名。即使不能说明名称是“ first first”还是“ last,first”或其他变体,但是如果名称的一个或两个部分为空白,并且对合并长度和如果超出了该怎么办,

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.