软件团队代码审查核心指南:心智模型对齐与需求层次金字塔

原文:Code Review Essentials for Software Teams | 作者:Blake Smith | 日期:2015-02-09

📝 摘要

代码审查(Code Review)是任何协同软件开发团队不可或缺的核心基石。Sprout Social 首席工程师 Blake Smith 在本篇经典长文中指出:代码审查的最核心价值既非单纯捕获 Bug,亦非代码风格挑刺,而是让团队成员在系统持续演化中保持「心智模型(Mental Model)」的同步对齐。文章提出了著名的「代码审查需求层次金字塔」——以团队成员心智模型对齐为基座,向上依次是确保方案正确解决问题、权衡架构设计的优劣、在上线前拦截缺陷,而代码格式挑刺则位于最次要的塔尖。作者倡导「柳叶刀驱动开发(Scalpel-Driven Development)」,主张在编写任何改动前自问四个关键问题以实现细粒度拆解;通过翔实的正反例对比,详解了如何撰写消除认知摩擦的优质 PR 描述;并针对审阅者提出「以提问代替指责」的建设性反馈法则,力劝团队将代码风格全权交付自动化工具,将宝贵的人力认知资源投入到更高阶的架构设计思考中。

📋 术语表

英文中文说明
code review hierarchy of needs代码审查需求层次仿照马斯洛需求层次构建的代码审查价值金字塔:底座为心智模型对齐与正确性,塔尖为代码风格
mental model心智模型工程师在脑海中对系统工作原理、状态流转与模块交互构建的认知表象
scalpel-driven development柳叶刀驱动开发主张用精准细微的小粒度改动逐步演进代码,避免巨型变更引发理解与验证灾难的开发实践
constructive feedback建设性反馈针对具体问题以提问方式引导开发者思考、提供清晰改进方向而非空洞批判的审查意见
pull request (PR)拉取请求 / 合并请求协同开发中将分支变更提请团队成员进行代码审查并申请合入主分支的机制
style guide代码风格规范团队关于命名、缩进、大括号位置等表面代码格式的统一规约,推荐通过自动化工具强制执行而非人工审查挑刺
nitpicking / bike-shedding琐碎挑刺绕开架构设计与逻辑正确性等核心议题,而将大量时间耗费在命名、缩进等细枝末节上的低效审查倾向

正文(双语对照)

Code Review is an essential part of any collaborative software project. Large software systems are usually written by more than one person, and so a highly functioning software team needs a robust process to keep its members, as well as the code base itself moving in the right direction.

代码审查(Code Review)是任何协同软件开发项目不可或缺的核心基石。大型软件系统通常是由多人协同构建而成的,因此,一个高效运转的技术团队必须拥有一套稳健严密的流程,以确保团队成员的心智理解以及代码库本身始终沿着正确的方向向前演进。

Code Review is a powerful tool that:

  1. Helps team members adapt their mental model of the system as it’s changing
  2. Ensures the change correctly solves the problem
  3. Opens discussion for strengths and weaknesses of a design
  4. Catches bugs before they get to production
  5. Keeps the code style and organization consistent

代码审查是一件极为强大的工程工具,它能够:

  1. 帮助团队成员在系统不断变化的过程中,持续校准和更新自己对系统架构的心智模型;
  2. 确保本次代码变更能够准确、恰当地解决目标问题;
  3. 为架构方案的优缺点权衡开辟开放深入的探讨空间;
  4. 在代码正式进入生产环境前提前拦截各类缺陷;
  5. 维护代码库风格与组织架构的高度一致性。

It’s helpful to think of these benefits as a hierarchy of needs.

将上述诸多收益理解为一种类似马斯洛需求层次的「金字塔模型」,会对我们大有裨益。

图 1:代码审查需求层次金字塔(从基座的心智模型对齐、正确性、设计探讨、捕获 Bug,到塔尖的代码风格)

心智模型对齐:维系团队协同的最核心力量

The most critical function of a Code Review is to keep every member of the team moving in the right direction. You can’t safetly change a system you don’t understand, and so Code Review keeps the team mentally aligned together. When Bob submits a pull request for the accounting subsystem, Amy keeps her mental model of that system updated as she reviews Bob’s code. Amy gets the chance to ask questions about pieces she doesn’t understand, and Bob gets the benefit of clarifying his design decisions as well as teaching someone else about his work. When Amy has to make a change to the accounting subsystem a month later, her mental model of the system is already up to date and ready for action. She spends less time reading code and trying to piece the system together in her brain, and more time thinking about higher level abstractions and designs. Everyone wins, because everyone stays together.

代码审查最关键、最不可替代的功能,就是确保团队中的每一位成员始终并肩前行在同一条轨道上。你绝不可能安全地修改一个自己根本无法理解的系统,正因如此,代码审查是维系团队整体心智模型高度对齐的纽带。当 Bob 提交了一个涉及账务子系统的 Pull Request 时,Amy 在审阅这批代码的过程中,同步更新了自己对该子系统最新运转机制的心智模型。Amy 借此机会对尚未参透的部分发起提问,而 Bob 也得以阐明自己的设计决策,并顺理成章地将知识沉淀传递给同事。一个月后,当轮到 Amy 需要对账务子系统进行功能扩展时,她脑海中对该系统的认知早已是最新的、随时可以付诸实践。她不必再耗费大量时间重新啃读源码并在大脑中痛苦拼凑碎片,而是可以把宝贵的脑力投入到更高维度的抽象与架构构思中。每个人都是赢家,因为整个团队始终紧紧凝聚在一起。

打造优质 Pull Request:动笔前的自问四准则

Before you even write a line of code changing the system, ask yourself the following questions:

在为你计划的系统改动敲下哪怕第一行代码之前,先认真问自己以下四个关键问题:

  • “Is this the right thing to be working on?” There will always be competing needs from customers, internal team members and other parties. This can be a good way to keep priorities straight in your head before you dive in deep. Other processes like iteration / sprint planning meetings also help keep this on track.

  • “Does the team already agree that the change is the right one?” If not, it’d be better to start a design discussion by email or maybe in person. Your changes are more likely to get accepted when people are in agreement about the design before you change it.

  • “How can I break this change into digestable chunks that are easy to review and understand?” Small changes are easier to think about and understand. Good discussions flow from the team being able to comprehend your change quickly. If your change is massive, teammate’s eyes will glaze over, and you might only get a few style nitpicks from them.

  • “How am I going to test this change to kill bugs and ensure correctness?” You might have a QA department, but it’s still your job as the developer to ship quality working software. Easily testable software is usually more decoupled, is broken into smaller chunks, and easier to reason about. You need to have a testing plan for all your changes.

  • 「这是当前最值得投入精力去做的正确事情吗?」 无论何时,来自客户、团队内部成员以及各方利益相关者的诉求总是在激烈竞争优先级。在深陷代码细节之前自问这一句,是确保自己脑海中优先级清晰明确的绝佳方式。当然,迭代或 Sprint 规划会议等其他流程机制也能帮助确保这一目标的达成。

  • 「团队对于这项改动是否已经达成共识并予以认可?」 如果尚未达成共识,更明智的做法是先通过邮件或面对面沟通发起技术方案评审。当大家在代码正式动笔前就已经对设计方案达成一致时,你的改动被顺利接受并合并的概率将大得多。

  • 「我该如何将这项改动拆解为易于审阅和理解的小型独立模块?」 小粒度的变更新颖而精炼,极易被思考和消化。只有当团队同事能够快速理清你的思路时,高质量的深入讨论才会随之发生。倘若你直接甩出一个庞大臃肿的巨型变更,审阅者的目光很快就会呆滞麻木,最终你所能得到的往往只剩下几条不痛不痒的格式挑刺。

  • 「我打算如何对这项改动进行全面测试,以绞杀潜在 Bug 并确保证正确性?」 团队或许配备了专门的 QA 测试部门,但作为一线研发工程师,交付高质量且运转正常的软件始终是你的第一天职。易于测试的软件通常具备更优的解耦性、更小的模块粒度以及更清晰的逻辑推理路径。你必须为自己的每一次代码变更准备清晰完备的测试计划。

I’ve found that answering these questions ahead of time has saved me a lot of headache in the long run. The last thing you want to do is spend days coding up a change only to have it rejected based on fundamental design flaws or team disagreement. Or for your change to get held up because no one can verify it for correctness. Again, the goal is to change the system while keeping other team members up to date with your changes. Asking yourself about how you’re going to break your work into bite-sized chunks and test those chunks is helpful on many fronts:

  • It reduces risk
  • It makes changes easier to reason about
  • It pushes you towards a better design

长期的工程实践让我深切体会到:提前想清楚这几个问题,在长远来看能为你省去无数令人头疼的麻烦。你最不希望看到的,莫过于自己辛辛苦苦熬夜几天写出的代码,在评审阶段却因底层的根本性设计缺陷或团队分歧而被当头浇下一盆冷水、直接遭到否决;或是改动因无法被任何人验证其正确性而无限期搁浅。重申一遍:核心目标是在演进系统的同时,让团队同事始终与你的变更保持心智同步。时刻审视自己将如何把工作拆解成易于消化的颗粒度并加以测试,会在多个维度带来深远收益:

  • 大幅降低系统风险;
  • 让代码变更的推理与理解门槛断崖式下降;
  • 强力倒逼出更优雅的解耦架构设计。

You’d rather make many small, precise cuts with a scalpel than one giant gash with a machete. I prefer scalpel driven developement in most additive cases, and like to save the machete for when it’s time to delete large blocks of dead code.

我们宁愿使用精巧的手术刀完成许多次微小而精准的细微切口,也绝不应该拿着大砍刀在系统上留下一道狰狞的大创口。在绝大多数增加新特性的开发场景中,我坚决推崇「柳叶刀驱动开发(Scalpel-Driven Development)」;至于那把大砍刀,我更愿意把它尘封起来,留到必须大刀阔斧彻底切除大片废弃死代码的时刻再行出鞘。

发起 Pull Request:为审阅者扫清认知障碍

Ok, so you’ve gotten buy-in from the team that your changes are good, and you’ve achieved the design you set out to build. What’s the most effective way to actually send your pull request? You’ve been working hard on your changes, and you want other members of your team to pay attention to your pull request and give you fast feedback. How can you do that?

好了,现在你已经赢得了团队对方案的充分认同,并且顺利完成了预期架构的编码落地。那么,究竟怎样才是发起 Pull Request 最具杀伤力、最高效的姿势?你为这批代码倾注了大量心血,自然迫切期望同事们能够迅速聚焦你的 PR 并给予高价值、及时的审查反馈。你该如何做到这一点?

Remember earlier when I said that the most important part of a code review is to keep the team’s collective mental model well aligned? Your other teammates are probably working on something completely different than you, maybe in a completely different part of the system. Their brains are in a completely different context, so you must overcome this by giving them the helpful guidance they need to do the review. This means writing a well-organized description of the changes you made, why you made them and any other relevant information they won’t get from just reading the code alone. Don’t make your teammates do more mental work than they have to.

还记得我们前面强调的原则吗——代码审查最重要的使命就是保持团队整体认知心智的严丝合缝。在日常研发中,你的队友很可能正全身心扑在与你截然不同的模块或业务线上,甚至活跃在系统的另一端。他们的大脑此时处于完全异构的上下文之中。因此,你必须主动铺平道路,在 PR 中为他们提供开展审查所需的全部背景指引。这意味着你需要写下一份层次分明的高质量描述:清晰阐述你改动了什么、为什么要这样改,以及仅凭生硬源码绝对无法直接读出的关键业务背景信息。千万不要逼着你的队友去做不必要的脑力消耗与心智拼图。

Let’s look at some good and bad examples of pull request descriptions:

让我们来看看两组极具代表性的 PR 描述正反面教材:

Bad Example:

反面教材(糟糕的 PR 描述):

Title: Fix uninitialized memory bug
Description:
 
This is the bug Bob and I talked about earlier. I had
trouble with the compiler but managed to make this work. Let me
know what you guys think.
标题:修复未初始化内存 Bug
描述:
 
这就是我和 Bob 之前讨论过的那个 Bug。我在编译器那边遇到点麻烦,不过总算折腾跑通了。大家看看怎么样。

If you’re the developer of this pull request, stop and put yourself in your code reviewer’s shoes for a second. The title is vague, and raises more questions than answers: Where is the memory bug? How critical is this change? What was the bug that Bob and you talked about earlier? What trouble did you have with the compiler? The description doesn’t provide any helpful context about the problem, nor does it provide any helpful description of your changes. If this pull request is long, the reviewer is going to have to dive into the code and do mental gymnastics in an attempt to gain context before even getting a chance to think about how this change fits in to the coherent design of the system.

如果你是这个 PR 的作者,请立刻停下脚步,换位到审阅者的视角审视两秒钟。这个标题语焉不详,带来的问号远多于答案:这个所谓的内存 Bug 到底潜伏在系统的哪个角落?这项修复有多紧急、波及面多大?你和 Bob 之前关起门来私下聊的究竟是哪个具体问题?你在编译器上遭遇的麻烦又是怎么回事?整个描述既没有为问题本身补充任何有价值的背景,也没有对代码改动作出任何结构性说明。如果这个 PR 变更行数稍多,审阅者就不得不一头扎进茫茫源码里做痛苦的脑力体操,在尚未开始评估这项变更是否契合系统长期架构之前,就已经被匮乏的上下文折腾得筋疲力尽。

Here’s a improved example that helps clarify the changeset:

接下来,请看一段精心重构、极大扫清理解障碍的正面示范:

Title: Fix process crash on startup from uninitialized memory [#54633]
Description:
 
This bug was causing process crashes on boot due to a memory
initialization error in our statistics Counter class. I talked this
over with Bob, and we both agree the crashes are a rare edge case
that don't warrant a hot-fix release. Here's a summary of the
changes:
 
  - Moved the underlying int variable into the class initializer
    to prevent ununitialized memory in the Counter.
  - Reworked the Counter interface to simplify caller conditional
    logic and prevent further off-by-one counting problems.
  - Added a unit test that exposes the crash
 
Testing: I've verified the test suite still passes, and verified
manually that the crash doesn't happen locally.
标题:修复统计类未初始化内存导致的进程启动崩溃 [#54633]
描述:
 
该 Bug 由于统计计数器(Counter)类中的内存初始化缺陷,会导致进程在系统引导时偶发崩溃。我已与 Bob 深入同步了背景,我们一致认为此崩溃属于极小概率的边缘极端场景,无需立即发布热修复补丁。以下是核心变更点提要:
 
  - 将底层的 int 变量显式收归类初始化器内部,彻底根除 Counter 中的内存未初始化隐患。
  - 重构了 Counter 对外接口,简化调用方的条件判断分支,杜绝后续潜在的差一(Off-by-one)计数偏差。
  - 补充了能精准复现并拦截该崩溃场景的单元测试用例。
 
测试验证:已确认全量测试套件持续通过,并在本地环境手动验证确认启动崩溃现象已被消除。

A few things that have improved in this pull request description: The title is descriptive enough to give the reviewer some short context, and entice them to click on the email and learn more. The reviewer knows it’s a process crash (which is usually a really bad thing) and there’s also a bug report number where the reviewer can read more details about the bug report if they’re interested in understand more about the problem. The new description also outlines where the problem was occuring, and helps give some context about how critical the fix is. There’s a summary that lists the high level structural changes in the pull request, which will give the reviewer a mental picture of the changes before they even look at the code. Code reviewers should not be surprised with what they find. This example also outlines testing that was performed, which will give reviewers confidence that the changes were well thought out, well tested and ready to be merged. Set your reviewers up for success by making it stupid easy for them to click ‘merge’.

在这份重构后的 PR 描述中,多处关键细节发生了质的飞跃:标题具备充足的信息密度,能在瞬间为审阅者构建清晰的背景画像,吸引他们在收到通知邮件时愿意第一时间点击查阅。审阅者能立刻明白这是致命的进程级崩溃(这在工程上通常属于最高警报),并附带了清晰的 Issue 单号,方便有探究欲的同事随时点击展开更详尽的前因后果。新描述精准框定了问题发生的具体位置(Counter 类),清晰交代了修复紧迫度等级。提纲挈领的变更摘要梳理了高层结构调整,让审阅者在真正打开 diff 源码之前,脑海中就已经建立起了一幅清晰全景图——在代码审查中,绝不应该让审阅者面对源码产生突兀的惊诧。此外,该范例还严谨罗列了所完成的测试验证工作,极大增强了审阅者对这批改动「深思熟虑、测试充分、随时可合入」的信心底气。让点击「Merge」按钮变得极致简单和无脑,才是对审阅者最深沉的关怀与敬意。

审阅者修养:提问式建设性反馈

The next part of the review process to consider is giving constructive feedback to your peers. If the goal is clarity and alignment, giving quality constructive feedback helps everyone understand the system better and push towards better code.

审查流程的下一个核心考量,是如何向你的同行同僚给出真正有建设性的反馈。如果我们的终极目标是认知清晰与心智对齐,那么高质量的建设性反馈不仅能帮助每个人加深对系统的理解,更能有效推动整个工程团队持续产出更高品质的代码。

Avoid saying things like:

  • “This design is broken.” Why is it broken? How can it be made better? Statements like this hurt confidence and can bruise egos.
  • “I don’t like this change.” Why don’t you like it? What would you like instead? It’s okay to not like something, but you should articulate your thoughts and provide helpful clues about how the code can be improved.
  • “Can you rewrite this to be more clear?” What’s wrong with what I have now? How should I rewrite it? What’s unclear? Comments like this are themselves unclear, and don’t give a simple path forward.

在审查批注中,请坚决避免说出类似以下情绪化且空洞的武断评语:

  • 「这个设计完全是烂掉的(This design is broken)。」 它到底烂在哪里?怎样改造才能变得更好?这种毫无建设性的宣判极度挫伤队友的研发信心,并极易引发对立防御情绪。
  • 「我不喜欢这个改动。」 你的顾虑究竟是什么?你更倾向于哪种替代方案?心存顾虑或偏好完全正常,但你必须清晰阐明背后的逻辑支撑,并为代码的优化给出切实可行的线索。
  • 「你能把这块重写得更清楚点吗?」 我当前的代码到底哪里晦涩?应该按什么思路重写?究竟是哪一部分缺乏可读性?这类评语本身就极具歧义,根本无法为作者指明明确的改进路径。

Instead, say things like:

  • “How does this code handle negative integers?” This feedback is specific, and causes the developer to think through the outcomes themselves. As a reviewer, you might know that the code in question crashes with negative integers, but it’s better to have the developer intuit this themselves. Questions like this might also be indicative of a testing gap, or a need for further specification of scope.
  • “This section is confusing to me, I don’t understand why class A is talking to class B” If the developer hasn’t provided a helpful description, and the code isn’t neatly structured, this kind of comment helps drive for clarity of design and cleaner code.
  • “It looks like you broke an interface boundary here. How will that affect the user?” You’ve pointed out an issue you noticed, giving them the benefit of the doubt that they meant to do it. Now they can think through the unseen ramifications of breaking the interface boundary and either provide a rationale for their decision, or choose to change it.

相反,请学会将反馈包装为启发式提问,采用类似以下客观探讨的句式:

  • 「当输入为负整数时,这段逻辑会如何运转?」 这类反馈极其具体,能够促使开发者自己顺着逻辑推演可能引发的后果。作为审阅者,你心里可能早已清楚传入负数会导致进程崩溃,但通过提问引导开发者自己直觉式地推导出来,效果要好得多。同时,此类提问还能敏锐揭示出测试覆盖的盲区,或促使团队进一步明确业务边界。
  • 「这一段让我产生了一些困惑,我不太理解为什么 A 类需要直接依赖并调用 B 类?」 当开发者未能提供充分的上下文且模块结构略显混沌时,这类坦诚表达自身阅读体验的评论,能够有力促使双方重新审视架构设计的纯粹性,倒逼出更干净的代码结构。
  • 「看起来这里打破了原有的接口契约边界。这会对上层用户产生哪些潜在影响?」 你指出了观察到的结构异常,但同时给予了对方充分的善意假设(假定对方可能存在深思熟虑的特定动机)。如此一来,作者便能重新权衡打破边界所带来的隐性连锁反应,要么从容给出充分的设计理由,要么主动选择回归规范。

In general, framing feedback as questions is a good way to drive for clarity, correctness, and at the same time help the developer improve their designs in the future. This is usually how quality creative writing groups give each other feedback. In a creative writing setting, it’s harmful to say things like, “I don’t like this character.” whereas the same comment can be reframed more clearly: “In chapter one your character was warm and compassionate and now he’s cold and icey. He doesn’t seem like a real person to me.” Now there’s specific feedback that can be clarified to suss out the problem.

总体而言,将审查反馈框架化为开放式提问,是一剂推动逻辑严密、确保功能正确,同时长线助力开发者提升架构设计素养的灵丹妙药。这与顶尖文学创意写作研习社成员间的互评之道异曲同工。在文学创作探讨中,如果随口抛出一句「我不喜欢这个人物」,不仅有害而且毫无助益;但如果换一种精准具体的表达:「在第一章里,这个角色展现得温暖而充满同情心,但到了第三章却突然变得冷酷无情。在我看来他显得有些割裂、不够立体真实。」你看,此时便拥有了一条具体明确的线索,作者能够立刻针对这处断层展开推敲与打磨。

Programmers like to solve problems and point out issues, so by nature they like to discover and point out flaws. It’s tempting to see code reviews as a way to prove how smart you are by finding problems in your peer’s code. Don’t do it. Code review is a way to get more eyes on a change and suss out critical problems, but your goal should be to review in a way that encourages your team members to improve their skills while fixing the problems at hand.

程序员天生以解决难题、排查故障为乐,骨子里就流淌着嗅探漏洞并指出破绽的极客本能。这使得人们很容易滋生一种隐秘的虚荣心:试图把代码审查当成通过挑同僚代码毛病来显摆自己多聪明的竞技场。千万不要这么做。代码审查的本质是通过引入更多视角来共同分担风险、排除隐患,你的终极目标应当是以一种能够激发团队伙伴精进技艺、相互成就的方式开展审查,在解决眼前工程问题的同时,滋养整个团队的向上心智。

风格细节:交给自动化工具,将人类解放于高阶思考

Brace positions, variable / function names, indentation and spacing issues should be addressed, but they are not the central purpose of good code review (notice that I put them at the top the code review pyramid). If you find that your team is spending 90% of it’s time nitpicking indentation and variable names, you’re probably wasting everyone’s time on something that could be mostly automated. Write up a style guide, enforce indentation and spacing issues on check-in and spend your time focusing on higher value issues. I don’t want to diminish the importance of a consistent style. On the contrary, having a consistent idiomatic style is one of the easiest ways to make your codebase easy to read and comprehend. Still, if you spend all of your code review focused on these simplistic tasks, ask yourself whether you’re avoiding the harder and more important work of keeping your team mentally aligned and thinking about higher level designs.

大括号的摆放位置、变量与函数的命名偏好、代码缩进以及空格排版确实需要得到规整,但它们绝不是一次高质量代码审查的核心意义所在(留意我特意将它们置于代码审查金字塔最顶端那块最窄的区域)。如果你发现自己的团队在日常 PR 中,竟然耗费了 90% 的精力和口舌在揪着缩进细节或命名琐事喋喋不休,那么你们很可能正在把所有人宝贵的精力,白白浪费在完全可以通过工程自动化轻松搞定的事情上。

制定一份统一的代码风格指南(Style Guide),在 Git Commit / CI 门禁环节通过 Linter 与格式化工具强制自动化校验,将人类的注意力集中在更具高商业与架构价值的核心议题上。我绝不是在贬低代码风格一致性的重要性;恰恰相反,维持一致且地道的风格规范,正是让整个代码库变得清晰易读的最简捷路径。然而,如果你们团队的代码审查始终沉溺在这些低智机械的表层挑刺中,不妨深刻反思一下:你们是否正在下意识地逃避那些真正艰难、却也重要得多的核心使命——维护团队的心智模型统一,以及推敲更高维度的系统架构设计?

关于作者

Blake Smith is a Principal Software Engineer at Sprout Social.

Blake Smith 是社交媒体管理平台 Sprout Social 的首席软件工程师(Principal Software Engineer)。