博客 / 工程

测试通过了,为什么还要再看 Diff?

author avatar
Alexander Kuzmenkov
2021年4月14日 - 分钟阅读

代码审查是少数几种被持续发现可以减少缺陷发生率的软件开发技术之一。为什么它有效?本文就此主题提供了一些大胆的猜想,并附带了关于如何最大限度地利用代码审查的实用建议。

理解你的程序为什么能工作

作为软件开发人员,我们经常需要推断软件的行为。例如,为了修复一个错误,我们首先从一个展示该行为的测试用例开始,然后阅读源代码,看看这种行为是如何产生的。通常,我们发现自己什么也理解不了,不得不求助于取证技术,例如使用调试器或询问代码的作者。这种情况远非理想。毕竟,如果我们理解我们的软件都有困难,我们怎么能确定它 আদৌ能工作呢?难怪它不能。

正确的理解在修改和扩展软件时也很重要。程序员必须始终对程序中发生的事情,它如何精确地映射到领域等等有一个精确的心理模型。如果这个模型中有缺陷,他们编写的代码将与领域不匹配,并且无法正确解决问题。错误的理解直接导致错误。

我们如何才能使我们的软件更容易理解?人们常说,要看你是否真的理解了某件事,你必须试着向别人解释它。例如,作为一个参加考试的理科学生,你可能被期望对一些著名的观察到的效应给出解释,并从这个领域的基本定律中推导出来。以类似的方式,如果我们在软件中建模某个问题,我们可以从领域知识和一般的编程知识开始,并建立一个论证,说明为什么我们的模型适用于这个问题,为什么它是正确的,具有最佳的性能等等。这种解释以代码注释的形式,或者在更高的层次上,以设计文档的形式。

如果你有彻底注释你的代码的习惯,你可能已经注意到,写注释往往比写代码本身更难。它也有一个令人不快的副作用——有时,在写注释的时候,你越来越清楚地意识到,这段代码是难以理解的,需要永远才能解释清楚,或者可能是完全错误的,你必须重写它。这正是写注释的主要积极作用。它帮助你找到错误,使代码更容易理解,如果你不尝试解释代码,你不会注意到这些问题。

理解你的程序为什么能工作与理解它为什么会失败是不可分割的,所以对于后者有一个类似的过程也就不足为奇了,这个过程叫做“橡皮鸭调试”。为了调试一个特别棘手的错误,你开始逐步向一个假想的伙伴,甚至是向一个无生命的物体,比如一个黄色的橡皮鸭,解释程序逻辑。这个过程通常非常有效,远远超出了人们对橡皮鸭有限的会话能力的预期。其潜在机制可能与注释相同——你只是通过尝试解释你的程序而开始更好地理解它,这让你能够找到错误。

当在一个团队中工作时,你甚至可以奢侈地向另一个在同一个项目上工作的开发人员解释你的代码。这可能比和鸭子说话更有趣。更重要的是,他们将要维护你写的代码,所以最好确保*他们*也能理解它。解释你的代码如何工作的一个好的正式场合是代码审查过程。让我们看看你如何能最大限度地利用它,在使你的代码可理解方面。

审查他人的代码

代码审查通常被框架为一个门禁过程,每个贡献都由维护者审查,以确保它符合项目方向,具有可接受的质量,符合编码指南等等。当处理外部贡献时,这种观点可能看起来很自然,但如果你把它应用于内部贡献,就显得不太合理了。毕竟,我们的同事维护者完全理解项目目标和指南,他们可能比我们更有才华和经验,并且可以被信任来产生最佳解决方案。额外的审查如何能有所帮助?

审查代码的一个不太明显但非常重要的部分是,看看它是否能被另一个人理解。无论当事人的行政角色和编程熟练程度如何,这都很有帮助。如果易于理解是你的主要优先事项,作为审查者,你应该怎么做?

你可能不需要关心代码风格之类的小事。有自动化工具可以做到这一点。你可能会发现一些错误,但这可能是一个副作用。你的主要任务是理解代码。

首先检查 pull request 试图解决的问题的高级描述。阅读它修复的错误的描述,或者它添加的功能的文档。对于更大的功能,通常会有一个设计文档,描述整体实现,而不会过于深入代码细节。在你理解问题之后,开始阅读代码。它对你来说有意义吗?你不应该太努力地去理解它。想象一下你很累,并且时间紧迫。如果你觉得你必须付出很多努力才能理解代码,请向作者澄清。当你交谈时,你可能会发现代码不正确,或者可以以更直接的方式重写,或者需要更多的注释。

hidden-items.png

在你得到答案后,不要忘记更新代码和注释以反映它们。不要仅仅在别人向你解释清楚后就停止。如果你作为审查者有一个问题,很可能其他人以后也会有这个问题,但可能没有人可以问。他们将不得不求助于 git blame 并重新阅读整个 pull request 或其中的几个。代码考古学有时很有趣,但当你正在调查一个紧急错误时,这是你最不想做的事情。所有的答案都应该在表面上。

与作者合作,你应该确保对于任何具有基本领域和编程知识的人来说,代码大部分是显而易见的,并且所有不显而易见的部分都得到了清晰的解释。

准备你的代码以供审查

作为作者,你也可以做一些事情,使你的代码更容易被审查者理解。

首先,如果你正在实现一个主要功能,在你甚至开始编写代码之前,可能需要一轮设计审查。跳过设计审查,直接进入代码审查可能是一个主要的挫折来源,因为它可能会发现即使你正在解决的问题也被错误地表述了,你所有的工作都必须被抛弃。当然,设计审查也不能完全防止这种情况。编程是一项迭代的、探索性的活动,在复杂的情况下,你只有在实现了第一个解决方案后才开始掌握问题,然后你意识到这个解决方案是不正确的,必须被抛弃。

当准备你的代码以供审查时,你的主要目标是使你的问题及其解决方案对审查者清晰可见。一个好的工具是代码注释。任何相当大的逻辑片段都应该有一个介绍性注释,描述其一般用途并概述实现。这个描述可以引用类似的功能,解释它们之间的区别,解释它如何与其他子系统接口。放置这个一般描述的一个好地方是充当功能主要入口点的函数,或其他形式的公共接口,或最重要的类,或包含实现的文​​件等等。

深入到每个代码块,你应该能够解释它做什么,为什么这样做,为什么是这种方式而不是另一种方式。如果有几种方法可以做这件事,你为什么要选择这种方法?当然,对于某些代码,这些事情从更一般的注释中得出,不必重复说明。数据操作的机制应该从代码本身中显而易见。如果你发现自己正在解释语言的某个特定功能,最好不要使用它。

特别注意使数据结构在代码中显而易见,并对其含义和不变性进行充分注释。数据结构的选择最终决定了你可以应用哪些算法,并设置了性能的限制,这也是我们作为 ClickHouse 开发人员应该关心它的另一个原因。

在解释代码时,重要的是给你的读者足够的上下文,以便他们可以在不深入调查周围系统和晦涩的测试用例的情况下理解你。给出所有可能与任务相关的事物的指针。如果你知道你的代码必须处理的一些角落情况,请足够详细地描述它们,以便可以重现它们。如果有一个相关的标准或设计文档,请引用它,甚至内联引用它。如果你依赖于其他系统中的某些不变性,请提及它。当这样做很容易时,添加反映你的注释的程序化检查是一个好的做法。你关于不变性的注释应该伴随一个断言,重要的场景应该通过一个测试用例来重现。

不要担心过于冗长。通常注释不够,但几乎从不太多。

关于代码注释的常见担忧

经常听到对注释代码的想法的反对意见,所以让我们讨论几个常见的意见。

自文档化代码

你经常可以看到一个令人困惑的想法,即源代码可以以某种方式“自文档化”,或者注释是“代码异味”,它们的存在表明代码写得很糟糕。我很难想象这种信念如何能与多年来在维护足够复杂和大型的软件方面的任何经验,与他人合作的经验相容。代码和注释描述了解决方案的不同部分。代码描述了数据结构及其转换,但它不能传达含义。代码中的名称充当指针,将数据及其转换映射到领域概念,但它们是示意性的,缺乏细微之处。编写使人们容易理解数据操作方面正在发生什么的代码并不那么困难。它主要需要克制,也就是说,阻止自己过于聪明。对于大多数代码,很容易看出它做什么,但为什么?为什么是这种方式而不是那种方式?为什么它是正确的?为什么这里的快速路径有帮助?你为什么要选择这种数据布局?如何保证这种不变性?等等。对于一个独自在一个短期项目上工作的开发人员来说,这可能不那么明显,因为他们脑子里有所有必要的上下文。但是当他们不得不与其他人(甚至来自过去和未来的自己)一起工作,或者在一个不熟悉的领域工作时,非代码的、更高层次的上下文的重要性变得痛苦地清晰起来。我们应该,甚至可以,以某种方式将诸如 这个 这样的注释编码到名称或控制流中的想法是荒谬的。

过时的注释

注释不能被编译器或测试检查,因此没有自动化的方法来确保它们与其余注释和代码保持同步。注释逐渐变得不正确的可能性有时被用作反对任何注释的论据。

这个问题不是注释独有的——代码也可能并且确实会变得过时。诸如死代码之类的简单情况可以通过静态分析或研究代码的测试覆盖率来检测。更复杂的情况只能通过校对来发现,例如维护一个不再重要的不变性,或者准备一些不需要的数据。

虽然过时的注释可能会导致错误,但缺乏注释的情况也是如此,甚至可能更严重。当你需要一些关于代码的更高级别的知识,但它没有被写下来时,你被迫从第一原则开始进行整个调查,以了解发生了什么,这很容易出错。即使是过时的注释也可能比没有注释提供更好的起点。此外,在大量使用注释的代码库中,注释往往是大部分正确的。这是因为开发人员依赖注释,阅读和编写注释,在代码审查期间关注它们。注释通常会随着代码的更改而更改,过时的注释很快就会被注意到并修复。这确实需要一些习惯。在浩瀚的难以理解的自文档化代码沙漠中,孤立的注释不会有好结果。

结论

代码审查使你的软件更好,这其中很大一部分可能来自于尝试理解你的软件实际做了什么。通过特别注意代码审查的这一方面,你可以使其更加高效。你将减少错误,并且你的代码将更容易维护——作为软件开发人员,我们还能要求什么呢?

2021-04-13 Alexander Kuzmenkov。标题照片由 Nikita Mikhaylov 拍摄

附言:本文包含作者的个人观点,并非 ClickHouse 维护人员的权威手册。

分享这篇文章

订阅我们的新闻简报

随时了解功能发布、产品路线图、支持和云服务!
正在加载表单...
关注我们
X imageSlack imageGitHub image
Telegram imageMeetup imageRss image