How to Do Code Reviews Like a Human (Part One)

Michael Lynch

如何像人一样做代码评审(上)

原文由 Michael Lynch 发布,订阅该博客

最近,我一直在阅读关于代码评审最佳实践的文章。我注意到,这些文章几乎只关注如何找出 bug,而排除了评审中几乎所有其他环节。以建设性、专业的方式沟通你发现的问题?不重要!只要把所有的 bug 都找出来,剩下的自然会水到渠成。

于是我灵光一现:既然这套方法对代码管用,为什么不能用在谈恋爱上?为此,我宣布推出一本帮助开发者经营爱情生活的新电子书:

电子书封面

我的这本革命性电子书将教你行之有效的技巧,帮你最大程度地找出伴侣身上的缺点。本书包含以下内容:

  • 以同理心和理解与伴侣沟通问题。
  • 帮助伴侣改善他们的弱点。

根据我对代码评审文献的研读,关系的这些部分是显而易见的,不值一提

你觉得这会是一本好书吗?我想你刚才一定脱口而出:“不不不不不!”

那么,为什么我们谈论代码评审时却是这套话术呢?

我只能假设我读到的那些文章来自未来,那时所有开发者都是机器人。在那个世界里,你的队友会欣然接受那些措辞随意的代码批评,因为处理这些信息能温暖他们冰冷的机器人心脏。

在此我要大胆假设,你想改善的是当下的代码评审,而你的队友都是有血有肉的人。我还要更大胆地假设,与同事保持良好的关系本身就是目的,而不仅仅是为了降低单个缺陷成本而去调节的变量。在这样的前提下,你的评审方式会发生怎样的改变?

在本文中,我将探讨一些技巧,它们把代码评审不仅视为一个技术过程,同时也视为一个社交过程。

什么是代码评审?

“代码评审”这个词可以指代一系列活动,从站在队友身后一起看代码,到 20 个人逐行剖析代码的会议都算。而我在本文中所指的,是一个正式的、书面的,但又不像一系列面对面代码检查会议那样繁重的流程。

代码评审流程

代码评审的参与者包括作者——编写代码并提交评审的人,以及评审者——阅读代码并决定其何时可以合入团队代码库的人。一次评审可以有多位评审者,但为简单起见,我假设你就是唯一的评审者。

在代码评审开始之前,作者必须创建一个变更集。这是一组作者希望合入团队代码库的源代码修改。

当作者将变更集发送给评审者时,评审便开始了。代码评审以轮次进行。每一轮都是作者与评审者之间一次完整的往返:作者发送变更,评审者对这些变更给出书面反馈。每次代码评审都包含一轮或多轮。

当评审者批准这些变更时,评审结束。这通常被称为给出 LGTM,即“looks good to me(在我看来没问题)”的缩写。

为什么这很难?

如果一位程序员给你发送了一个他自认为很棒的变更集,而你却回了一长串理由说明它并不怎么样,那这条信息就很难传达得恰到好处。

这就是我一点也不怀念 IT 行业的原因之一,因为程序员是很不讨人喜欢的一群人……比如在航空领域,那些严重高估自己技能水平的人都已经死了。

-Philip Greenspun,ArsDigita 联合创始人,摘自 Founders at Work

作者很容易把对代码的批评解读为对自己编程能力不足的暗示。代码评审本应是分享知识、做出明智工程决策的机会。但如果作者将讨论视为人身攻击,这一切就无从谈起。

好像这还不够难,你还面临着用文字表达想法的挑战,而文字沟通更容易产生误解。作者听不到你的语气、看不到你的肢体语言,因此更需要谨慎地措辞。对于一位正处于防备状态的作者来说,一句无心的批注,比如“你忘了关闭文件句柄”,可能会被解读为“我真不敢相信你居然忘了关闭文件句柄!你真是个笨蛋。”

技巧一览

  1. 让计算机去做枯燥的部分
  2. 用风格指南终结风格之争
  3. 立刻开始评审
  4. 先抓大局,再抠细节
  5. 多给出代码示例
  6. 永远不要说“你”
  7. 把反馈措辞为请求,而非命令
  8. 让意见有据可依,而非主观臆断

让计算机去做枯燥的部分

在会议和邮件等各种打断之下,你能专注于代码的时间本就稀少。而你的脑力和专注力更是捉襟见肘。阅读队友的代码是一项认知负荷很重、需要高度专注的工作。不要把这些宝贵的资源浪费在计算机就能完成的任务上,尤其是在计算机能做得更好的情况下。

空白字符错误就是一个明显的例子。对比一下,让人工评审者去发现缩进错误并与作者协作修正,与直接使用自动化格式化工具相比,需要付出多少精力:

需要人工评审者付出的精力使用格式化工具所需的精力
  1. 评审者搜索空白字符问题并发现缩进错误。
  2. 评审者写下批注指出缩进错误。
  3. 评审者重读自己的批注,确保措辞清晰且不带指责意味。
  4. 作者阅读批注。
  5. 作者修正代码缩进。
  6. 评审者确认作者已正确处理该问题。
什么都不用做!

右侧之所以是空的,是因为作者使用的代码编辑器会在每次按下“保存”时自动格式化空白字符。即便情况再糟一点,作者把代码发出评审,持续集成系统也会报告空白字符不正确。作者自行修复问题,评审者完全无需操心。

在你的代码评审中留意那些可以自动化的机械性任务。以下是一些常见的例子:

任务自动化解决方案
验证代码能否构建持续集成方案,例如 TravisCircleCI
验证自动化测试是否通过持续集成方案,例如 TravisCircleCI
验证代码空白字符是否符合团队规范代码格式化工具,例如 ClangFormat(C/C++ 格式化工具)或 gofmt(Go 格式化工具)。
识别未使用的导入或未使用的变量代码检查工具,例如 pyflakes(Python 检查工具)或 JSLint(JavaScript 检查工具)。

自动化能帮助你作为评审者做出更有价值的贡献。当你可以忽略某一整类问题时——比如 imports 的顺序或源文件命名规范——你就能把精力集中在更有意义的事情上,例如功能性错误或可读性方面的不足。

自动化对作者同样有益。它能让他们在几秒钟内而非几小时后发现粗心的错误。即时的反馈让错误更容易被吸取教训、修复成本也更低,因为作者脑中还保留着相关的上下文。而且,如果非得被指出犯了个愚蠢的错误,听到计算机的提示总比听到你的指责更能保住自尊。

与你的团队协作,将这些自动化检查直接集成到代码评审工作流中(例如 Git 中的 pre-commit hooks 或 GitHub 中的 webhooks)。如果评审流程要求作者手动运行这些检查,你就会失去大部分好处。作者难免偶尔会忘记,到头来你还是得继续去评审那些本该由自动化处理掉的简单问题。

用风格指南终结风格之争

在评审中争论风格是浪费时间。保持风格一致固然重要,但代码评审不是争论大括号该放哪里的场合。把风格之争从评审中剔除的最好方法,就是维护一份风格指南。

一场典型的风格之争

一份好的风格指南不仅定义命名规范、空白字符规则等表面要素,还会规定如何使用特定编程语言的特性。例如,JavaScript 和 Perl 功能繁多——实现同一逻辑有多种方式。风格指南会定义做事的“唯一正解”,避免团队中一半人用一套语言特性,另一半人用完全不同的另一套。

有了风格指南,你就不用在评审中浪费时间与作者争论谁的命名规范更好。直接以风格指南为准,然后继续往下即可。如果风格指南没有对某个问题作出规定,通常也不值得为此争论。如果你遇到了指南未涵盖且确实重要到需要讨论的风格问题,那就和团队一起商定。达成一致后,把决定记录到风格指南中,这样以后就再也不用重复讨论了。

方案一:采用现有的风格指南

在网上搜索,你能找到许多现成的、开箱即用的已发布风格指南。Google 风格指南是最知名的,如果它的风格不适合你,也可以找到其他的。通过采用现有指南,你无需从零开始付出高昂成本,就能享有风格指南带来的好处。

缺点在于,各组织会根据自身特殊需求来优化风格指南。例如,Google 的风格指南在使用新语言特性方面较为保守,因为他们拥有庞大的代码库,代码需要在从家用路由器到最新款 iPhone 的各种设备上运行。如果你是一家只有四个人、单一产品的初创公司,或许会更积极地采用前沿的语言特性或扩展。

方案二:逐步创建自己的风格指南

如果你不想采用现有的指南,也可以自己创建。每当代码评审中出现风格争论,就把问题抛给整个团队来决定官方规范应该是什么。达成一致后,将该决定写入风格指南。

我更倾向于将团队的风格指南以 Markdown 形式纳入版本控制(例如 GitHub Pages)。这样,对风格指南的任何修改都要走正常的评审流程——必须有人明确批准变更,团队中的每个人也都有机会提出顾虑。Wiki 和 Google Docs 也是可行的选择。

方案三:混合方案

结合方案一和方案二,你可以采用一份现有风格指南作为基础,然后再维护一份本地风格指南来扩展或覆盖基础指南。一个很好的例子是 Chromium C++ 风格指南。它以 Google C++ 风格指南为基础,并在其之上做了自己的修改和补充。

立刻开始评审

把代码评审当作高优先级事项来对待。实际阅读代码和给出反馈时可以慢慢来,但开始评审一定要立刻——最好在几分钟内就着手。

代码评审接力赛

如果队友给你发送了一个变更集,很可能意味着在你的评审完成之前,他们的其他工作都被阻塞了。理论上,版本控制系统允许作者创建分支、继续工作,然后再将评审中的变更正向合并到新分支中。但现实中,能高效做到这一点的开发者总共只有寥寥几人。对其他人来说,理清三方 diff 要花很长时间,甚至会抵消掉等待评审期间取得的任何进展。

当你立刻开始评审时,就会形成一个良性循环。你的评审周转时间将纯粹取决于作者变更集的大小和复杂度。这会激励作者发送小而聚焦的变更集。这些变更集对你来说更容易、也更愉快地评审,因此你会审得更快,循环便得以持续。

想象一下,你的队友实现了一个需要改动 1000 行代码的新功能。如果他们知道你能在约 2 小时内评审完一个 200 行的变更集,他们就可以把功能拆成每个约 200 行的变更集,在一两天内就全部合入。如果相反,无论大小你都要花上一天来完成所有代码评审,那么现在这个功能就需要一周才能合入。你的队友不想干等一周,因此他们会被激励去发送更大的评审,比如每个 500–600 行。这些评审的成本更高,反馈质量也更差,因为要在一处 600 行的变更中保持上下文,比在 200 行的变更中要困难得多。

每一轮评审的周转时间最长不应超过一个工作日。如果你正忙于更高优先级的事情,无法在一天内完成一轮评审,请告知队友,并给他们重新分配给其他人的机会。如果你被迫推掉评审的频率超过大约每月一次,那很可能意味着团队需要放慢节奏,以维持健康的开发实践。

先抓大局,再抠细节

在某一轮评审中写下的批注越多,就越容易让作者感到不堪重负。具体的上限因人而异,但一般来说,单轮评审中批注数量达到 20–50 条时就进入了危险区。

如果你担心让作者淹没在批注的海洋中,就在前几轮中只给出高层次的反馈。关注诸如重新设计类接口或拆分复杂函数之类的问题。等到这些问题解决之后,再去处理更细节的问题,例如变量命名或代码注释的清晰度。

一旦作者采纳了你的高层次意见,你那些细节层面的批注可能就不再适用了。通过将它们推迟到后面的轮次,你不仅省去了撰写措辞谨慎的批注所需的不小工作量,也让作者免于处理不必要的批注。这种方法还能将评审中关注的抽象层次分开,帮助你和作者以清晰、系统的方式逐步完成对变更集的评审。

多给出代码示例

在理想世界里,代码作者会对收到的每一次评审都心怀感激。这是一个学习的机会,也能帮他们避免犯错。但现实中,有许多外部因素可能导致作者对评审产生负面观感,甚至因你提意见而心生怨恨。也许他们正承受着赶进度的压力,因此除了你立刻盖章通过之外的任何反馈都像是阻碍。也许你们合作不多,所以他们不相信你的反馈是出于好意。

让作者对评审过程产生好感的一个好方法,就是在评审中找机会给他们送“礼物”。而所有开发者都喜欢收到什么样的礼物呢?当然是代码示例。

收到代码这份礼物

如果你把自己建议的一些改动直接写出来,减轻作者的负担,就能表明你作为评审者愿意慷慨地投入时间。

举个例子,假设你的某位同事不熟悉 Python 的 列表推导特性。他们发给你的一次代码评审中包含了以下几行:

urls = []
for path in paths:
  url = 'https://'
  url += domain
  url += path
  urls.append(url)

如果你回复“我们能用列表推导来简化一下吗?”,会让他们感到烦恼,因为他们现在不得不花 20 分钟去研究一个从未用过的东西。

他们会更乐意收到如下这样的批注:

考虑像这样用列表推导来简化:

urls = ['https://' + domain + path for path in paths]

这种技巧并不局限于一行代码的示例。我经常会自己拉一个分支来向作者展示一个较大的概念验证,比如拆分一个大函数或添加一个单元测试来覆盖额外的边界情况。

仅在改进清晰且无争议时才使用这种技巧。在上面的列表推导示例中,很少有开发者会反对减少 83% 的代码行数。相反,如果你写了一个冗长的示例来展示一个仅基于个人喜好而“更好”的改动(例如风格上的改动),代码示例就会让你显得强势而非慷慨。

每轮评审中将代码示例限制在两到三个以内。如果你开始替作者重写整个变更集,就等于在暗示你认为他们没有能力自己写代码。

永远不要说“你”

这一条听起来可能有点奇怪,但请听我说完:永远不要在代码评审中使用“你”这个词。

评审中达成的决定应该基于什么能让代码变得更好,而不是谁提出了这个想法。你的队友在变更集上投入了大量精力,很可能为自己的工作感到自豪。他们听到对自己工作的批评时,自然的反应是变得防备和维护。

用一种尽可能降低激起队友防备心理的方式来措辞。明确表明你批评的是代码,而不是写代码的人。当作者在批注中看到“你”时,注意力就会从代码转移到自己身上。这会增加他们把批评当作针对个人的风险。

考虑这样一条看似无害的批注:

你把‘successfully’拼错了。

作者可能会以两种截然不同的方式来解读这条批注:

  • 解读 1:嘿,老朋友!你把‘successfully’拼错了。不过我还是觉得你很聪明!可能只是个笔误吧。
  • 解读 2:你把‘successfully’拼错了,蠢货。

与下面这条省略了“你”的批注对比一下:

sucessfully -> successfully

后一条批注只是一个简单的更正,而不是对作者的评判。

幸运的是,要重写反馈以避免使用“你”其实很容易。

选项一:把“你”换成“我们”

把这个变量重命名为更具描述性的名字吗,比如 seconds_remaining

改为:

我们把这个变量重命名为更具描述性的名字吗,比如 seconds_remaining

“我们”强调了团队对代码的集体责任。作者可能会跳槽到另一家公司,你也可能会,但拥有这份代码的团队将以某种形式继续存在。当某件事明显需要作者自己去做时,说“我们”可能听起来有点傻,但傻总比带有指责意味要好。

搬沙发的漫画

选项二:去掉句子的主语

另一种避免使用“你”的方法是使用省略主语的简写:

建议重命名为更具描述性的名字,比如 seconds_remaining

你也可以用被动语态达到类似的效果。我在技术写作中通常对被动语态避之唯恐不及,但它可以是绕开“你”的一种有用写法:

这个变量应该被重命名为更具描述性的名字,比如 seconds_remaining

还有一种选择是将其表述为以“what about…”或“how about…”开头的问题:

把这个变量重命名为更具描述性的名字怎么样,比如 seconds_remaining

把反馈措辞为请求,而非命令

代码评审比日常沟通需要更多的技巧和谨慎,因为讨论很容易偏离主题,演变成人身争论。你可能会以为评审者会在评审中更加礼貌,但奇怪的是,我发现情况恰恰相反。大多数人从不会对同事说“把那个订书机递给我,然后给我拿罐汽水来。”但我却见过无数评审者用同样颐指气使的命令来提意见,比如“把这个类移到单独的文件中。”

在反馈时宁可显得过分温和一些。把你的批注措辞为请求或建议,而不是命令。

对比同一条批注的两种不同措辞:

措辞为命令的反馈措辞为请求的反馈
Foo 类移到单独的文件中。我们能把 Foo 类移到单独的文件中吗?

人们喜欢对自己的工作有掌控感。向作者提出请求能给他们一种自主感。

请求也让作者更容易礼貌地提出异议。也许他们的选择自有充分的理由。如果你把反馈措辞为命令,作者的任何反驳都会显得像是不服从。如果你把反馈措辞为请求或问题,作者只需回答你即可。

对比一下,根据评审者最初批注的不同措辞,对话会显得多么不同:

措辞为命令的反馈(对抗性)措辞为请求的反馈(合作性)
评审者:把 Foo 类移到单独的文件中。
作者:我不想这么做,因为那样它就离 Bar 类太远了。客户端几乎总是会一起使用这两个类。
评审者:我们能把 Foo 类移到单独的文件中吗?
作者:可以,但那样它就离 Bar 类太远了,而且客户端通常会一起使用这两个类。你觉得呢?

看看当你编造假想对话来证明观点把批注措辞为请求而非命令时,对话会变得多么更文明?

让意见有据可依,而非主观臆断

当你给作者提意见时,要同时说明你建议的改动以及改动的原因。与其说“我们应该把这个类拆成两个”,不如说“目前,这个类既负责下载文件又负责解析文件。根据单一职责原则,我们应该把它拆分为一个下载器类和一个解析类。”

将你的批注建立在原则之上,能以建设性的方式展开讨论。当你引用一个具体的理由时,比如“我们应该将这个函数设为私有,以最小化类的公共接口”,作者就不能简单地回应“不,我更喜欢我原来的写法。”或者说,他们可以这么说,但会显得很可笑,因为你已经说明了改动如何达成某个目标,而他们只是在陈述个人偏好。

软件开发既是一门艺术,也是一门科学。你并不总能用既定原则来精确表述一段代码到底哪里出了问题。有时代码就是丑陋或不直观,却很难说清原因。在这些情况下,尽你所能去解释,但要保持客观。如果你说“觉得这段很难理解”,那至少是一个客观的陈述;相比之下,“这段很混乱”则是一种价值判断,未必对每个人都成立。

尽可能以链接的形式提供佐证。你团队风格指南中的相关章节是最佳的链接。你也可以链接到语言或库的文档。高赞的 StackOverflow 回答也可以,但在权威文档之外走得越远,你的证据就越站不住脚。

第二部分

如果你喜欢这篇文章,请查看本文的下半部分,它侧重于如何在没有难看冲突的情况下圆满结束评审。其中包括以下技巧:

  • 处理超大型的代码评审,
  • 识别给予赞美的机会,
  • 尊重评审的范围,以及
  • 化解僵局。

如何像人一样做代码评审(下)


编辑:Samantha Mason。插图:Loraine Yow。感谢 @global4g 对本文早期草稿提供了宝贵的反馈。

本文章由 muse-spark-1.2-contributor 进行翻译

评论