Google开源项目eng-practices:其内部一直在用,可持续提升代码健康度

工具推荐 1779848912更新

0

前几天逛Github,我刷到了一个已经归档很久的Google工程实践文档项目。

里面有一份代码审查指南,据说是Google内部一直在用的东西,是他们代码质量能保持高水准的根基之一。

我从头到尾看了一遍,看完最大的感受就是:

他们把代码审查这件事想得,比大多数团队透彻太多了。

先问一个问题,你们团队做代码审查,核心目标是什么?

找bug?检查代码质量?还是确保代码风格一致?

这些都对,但都不是Google认为的最核心目标。

Google在他们的指南里写得非常清楚:

"The primary purpose of code review is to make sure that the overall code health of Google's code base is improving over time."

翻译过来就是——代码审查的首要目标,是确保整个代码库的健康度随时间持续提升。

注意这个"持续"。

不是你今天review完一个PR就完事了,而是每一次代码审查,都应该让代码库比之前更健康一点点。

长期积累下来,这种微小的改进会产生巨大的差异。

Google还说了一句特别有意思的话:

"There is no such thing as perfect code—there is only better code."

没有完美的代码,只有更好的代码。

这句话翻译过来很简单,但背后有深意——接受"足够好",比追求"完美"更有价值。

很多团队的代码审查,为什么最后变成了撕逼现场?

因为大家都在追求"完美"——审查者觉得这里可以更好,那里可以更优雅;开发者觉得自己的代码已经够好了为什么要改来改去。

然后就开始battle了。

但Google的观点是,审查者不应该要求作者打磨到完美

审查者应该做的,是在"需要继续改进"和"这个改动本身已经足够好"之间,找到平衡点。

他们原话是这么说的:

"Reviewers should not require the author to polish every tiny piece of a CL before granting approval. Rather, the reviewer should balance out the need to make forward progress compared to the importance of the changes they are suggesting."(翻译:审查者不应该在批准之前,要求作者打磨CL里的每一个小细节。审查者应该权衡"需要继续推进"和"改动的重要性"之间的关系。)

与其追求完美,不如追求持续改进。

一个改动,只要整体上让系统更健康了,就应该被批准。

而不是卡在那里非要作者把每个细节都打磨到极致。

因为追求完美,拖了三四天不让合并,开发者激情没了,改动的质量反而下降了。

这就是Google说的,没有完美代码,只有更好代码。

追求完美,反而会让你离完美更远。

Google在指南里还提了一个特别有格局的观点,我觉得大多数团队都没意识到。

代码审查是一个教学机会。

Google说:

"Code review can have an important function of teaching developers something new about a language, a framework, or general software design principles."(翻译:代码审查有一个重要的功能,就是教开发者一些关于编程语言、框架或者通用软件设计原则的新东西。)

代码审查不应该只是"你这里有问题,帮我改一下"这种模式。

它还应该是——这里有个更好的写法,你学到了吗?

这个设计原则你之前可能没注意,今天提醒你一下。

这个模式可以用在别的地方,回头你可以试试。

Google认为,分享知识是改善代码健康的一部分。

你review别人的代码,顺便教了对方一点东西,对方下次写代码就会更好。代码健康度就这么螺旋式上升了。

这个视角真的有点东西。

但我见过的大部分团队代码审查是什么样的?

要么是"你这里有问题",没有任何解释为什么。

要么是"我觉得这样不好",没有任何依据。

还有的是审查者通过挑毛病来彰显自己的水平,让开发者感觉很受伤。

这些都不是教学,这是权力展示。

Google关于代码审查速度的说法,也让我印象深刻。

他们说:

"At Google, we optimize for the speed at which a team of developers can produce a product together, as opposed to optimizing for the speed at which an individual developer can write code."(翻译:在Google,我们优化的不是一个开发者写代码的速度,而是一个开发者团队一起产出产品的速度。)

Google优化的不是个人写代码的速度,而是整个团队产出产品的速度。

这两个是不一样的。

个人写得快没用,如果你的代码审查要等三天,那整个团队就卡在这里了。

Google的硬性标准是:如果你不在高度专注的任务中,应该尽快响应代码审查请求,最多等到第二天早上。

一个比较合理的做法是:先回复一句"收到,我会在今天/明天处理",让对方知道你看到了。不用马上开始review,但要确保对方知道你会跟进。

Google还提了一个加速审查的技巧:LGTM with Comments(批准但还有评论)。意思是审查者觉得你整体OK可以合并了,但还有一些小建议想提出来。这种情况可以直接批准,不用等作者逐条回复所有评论。

这特别适合用在:审查者相信作者会自己处理好这些小问题,或者建议本身就是可选的(nit级别),或者作者和审查者处在不同时区——如果等作者回复完所有评论再批准,可能要再等一天。

Google还做了一个挺反直觉的观察:

"Most complaints about the code review process are actually resolved by making the process faster."(翻译:大部分关于代码审查流程的抱怨,通过加快审查速度就能解决。)

大部分关于代码审查流程的抱怨,其实只需要加快速度就能解决。

开发者抱怨的不是审查严格,而是响应太慢。

你想想看,如果你收到审查意见,reviewer说"这个地方可以更好",然后马上回复"好的我改",你什么感觉?

你会觉得这个人很严格,但你不会生气。

但如果reviewer说"这个地方可以更好",然后你等了三天他才回复,然后又提了一堆新意见,你什么感觉?

你会想——你早干嘛去了???

所以Google的结论是,与其降低标准,不如加快反馈速度。

快速反馈比完美反馈更能解决问题。

Google在怎么写评论这件事上,给了一个我觉得是常识但很多人做不到的建议:

评论代码,不要评论开发者。

他们给了个对比:

Bad:

"Why did you use threads here when there's obviously no benefit to be gained from concurrency?"(翻译:"你"为什么要在这里用线程?这明显没有任何并发的好处吧?)

Good:

"The concurrency model here is adding complexity to the system without any actual performance benefit that I can see. Because there's no performance benefit, it's best for this code to be single-threaded instead of using multiple threads."(翻译:这里的并发模型给系统增加了复杂度,但我看不到任何实际的性能收益。因为没有性能收益,这段代码最好还是用单线程,而不是多线程。)

看出来了吗?

Bad版本是在质疑人,"你"为什么要这样做。

Good版本是在分析代码,这个并发模型增加了复杂度但没有性能收益,所以建议改成单线程。

人不会觉得被攻击,代码才会被优化。

这个原则说起来简单,做起来真的很难。

我见过太多代码审查变成人身攻击的——"你这段代码写得真烂","你是不是没学过设计模式","你这个逻辑我完全看不懂"。

这些话不是在优化代码,这是在伤害同事。

Google还建议评论的时候要解释为什么

不只是说"这里不好",而是说"这里不好,因为xxx,建议改成yyy,因为zzz"。

让开发者理解背后的原因,他们才能真正学到东西,下次避免同样的问题。

还有一个小技巧值得提一下——标注评论的严重性。Google建议用不同的标签区分:

  • Nit: —— 这是一个小问题,不改也行,只是锦上添花
  • Optional: 或 Consider: —— 建议改,但不强求
  • FYI: —— 给你提个醒,不指望你这次改,但以后可以想想

这样作者就知道哪些是必须改的,哪些是可选的,不会把所有评论都当成命令。

Google在怎么写CL(Changelist,代码改动)这件事上,有一个我觉得特别重要的原则:

保持适度的小,每个CL只做一件事。

这里有个度的问题——不是越小越好,而是每个CL要"自包含",只解决一个问题。如果太小了也不行,比如你只加了一个API但没有包含使用示例,reviewer就很难理解这个API是拿来干嘛的。

为什么?

Google列了7个理由,我总结一下:

第一,小的CL审查得更快。reviewer很难找出整块时间看一个巨大的PR,但找几个5分钟的间隙看几个小PR就容易多了。

第二,小的CL审查得更仔细。reviewer看大PR看到后面就疲劳了,前面可能漏掉的细节就那么过去了。

第三,小的CL不太会引入bug。改动的范围越小,越容易理解和验证。

第四,小的CL被拒绝的损失更小。你写了个巨大的PR结果reviewer说方向错了,你会浪费好几天的工作;但如果是个小PR方向错了,你只浪费了一点时间。

第五,小的CL更容易合并。改动越大,冲突越多,处理起来越痛苦。

第六,小的CL更容易设计好。大拆解成小,每一块都容易想清楚。

第七,小的CL更容易回滚。大PR回滚可能牵连很多其他改动,小PR回滚就干净利落。

还有一点很重要:测试代码应该和功能代码在同一个CL里。你写了一个功能,同时就要把测试写好、提交。Google要求每个CL都必须有相应的测试代码。除非是紧急情况,否则不应该只提交功能代码而没有测试。

Google还在指南里特别提了一件事:

"Note that reviewers have discretion to reject your change outright for the sole reason of it being too large."(翻译:注意,审查者有权仅因为你提交的改动太大就直接拒绝。)

如果你的PR太大,reviewer有权直接拒绝。

所以与其被要求拆成小PR,不如一开始就写小PR。

这跟Google那句"没有完美代码只有更好代码"是一脉相承的——不要憋大招,一次改一点,持续进步

最后说一个细节,很多人不重视但Google非常重视的东西——CL描述

Google说CL描述会成为版本控制历史的永久组成部分,未来可能有成百上千人阅读这条记录。

所以CL描述必须清晰说明两点:第一,改了什么;第二,为什么改。

第一行要简短精炼,总结性的,说清楚CL具体做了什么,用祈使句。

Google给了个例子:

"Delete the FizzBuzz RPC and replace it with the new system."(翻译:删除FizzBuzz RPC,用新系统替换它。)

简洁明了,一看就知道干了什么。

主体部分要提供详细信息和补充说明,让reviewer和未来的读者理解CL的背景和逻辑。

Google还特别指出了差的CL描述是什么样的

  • "Fix bug"
  • "Fix build"
  • "Add patch"
  • "Moving code from A to B"
  • "Phase 1"

这些都是真实存在的CL描述,真的就是这么多字。

你看了什么感觉?

反正我看了是一头雾水,什么信息都没有。

好的CL描述,第一行让你知道做了什么,主体部分让你知道为什么这样做、有什么trade-off、有没有什么局限性和未解决的问题。

比如你选择了一个不是最优但实现起来更快的方案,这时候就要在描述里说清楚——"我选这个方案是因为时间紧迫,理想情况下应该用X方案重构"。这样未来的维护者就知道这段代码的来龙去脉了。

说在最后。

Google的代码审查指南内容远不止这些,但光是我今天聊的这些,已经足够让我重新审视我们习以为常的代码审查流程了。

核心就三句话:

第一,代码审查的目标是让代码健康度持续提升,不是追求完美。

第二,代码审查是教学机会,分享知识是改善系统的一部分。

第三,速度很重要,快速反馈比完美反馈更能解决问题。

下次你做代码审查,或者你的代码被审查的时候,想想这个——

你是在维护代码健康,还是在追求完美?

这两个的区别,可能比你想的大得多。

都看到这里了,如果觉得有收获,可以转发给团队的同学看看。