Skip to content

필사 모드: 代码评审的对话术:不带冲突地给予和接受反馈

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

引言 — 评审发生在人与人之间,而不只是代码之间

表面上看,代码评审是一项技术活动。你在找 bug、验证设计、统一风格。但凡在团队里待过一段时间的人都知道:评审之所以会失败,往往不是因为技术,而是因为。一个完全正确的指摘可能会破坏关系。一句无关紧要的评论可能演变成一场持续三天的对峙。一个评审请求可能被搁置数日,悄悄消磨作者的动力。

所以本文的前提很简单:代码评审本质上是沟通。 代码只是媒介,两位工程师之间真正流转的是判断、意图与信任。一旦接受这个框架,很多评审问题就会呈现出不同的样貌。「这个指摘对不对」固然重要,「这个指摘是怎么传达的」同样重要。

本文将逐一讨论如何不带摩擦地给予和接受反馈:评审代码而非评审人的框架、用 Conventional Comments 给评论贴标签、用提问代替命令、小 PR 的力量、称赞的作用、作为作者如何接受反馈,以及双方各自承担的责任。

评审代码,而不是评审人

评审中最根本的原则是,批评的对象是代码,而不是人。 这不仅仅是礼貌问题,更是准确性问题。因为评审的目的是改进代码库,而不是评价作者。

同一个指摘,框架不同,听起来会完全不同。

  • 「你这里漏掉了错误处理。」→ 是射向作者的箭。会让人产生防御心理。
  • 「这条路径上似乎缺了错误处理,要不要一起看看?」→ 指向代码。是一种共同查看的邀请。

关键工具是改变主语。把「你」换成「我们」或「这段代码」。

  • 把「你为什么这么写?」换成 → 「我很好奇这里为什么这样处理。」
  • 把「你这个函数太长了。」换成 → 「把这个函数拆一下,读起来可能会更容易。」
  • 把「你引入了一个 bug。」换成 → 「这段代码在空列表上似乎会抛出异常。」

我们是站在同一边,一起看着代码这个问题,而不是面对面互相瞄准。

这种「我们」的框架不只是一种委婉说法。它反映的是确实为真的事情:代码一旦合并,就属于整个团队。那个 bug 很快就会变成大家共同的 on-call 通知。所以「我们」是一个诚实的用词。作者与评审者是为代码质量这一共同目标而协作的伙伴。

nit 与 blocker — Conventional Comments

评审中一个常见的误解是,以为每条评论的分量都一样。 从作者的角度看,一个关于变量命名喜好的琐碎建议,和一个会导致数据丢失的严重缺陷,呈现为一模一样大小的评论框,并排显示。根本分不清哪个必须修、哪个可以忽略。这种模糊正是摩擦的一大来源。

Conventional Comments 用标签解决了这个问题。这是一个简单的约定:在每条评审评论前加上一个标签,标示这条评论的意图与严重程度。常见的标签有:

  • nit: 非常细枝末节的指摘。改了更好,但不改也不影响合并。(例如:细微的风格问题。)
  • suggestion: 改进建议。是否采纳由作者自行判断。
  • question: 真正的问题。当你没理解或想确认意图时使用。
  • blocker:(或 issue)必须解决才能合并的问题。
  • praise: 称赞。明确指出做得好的地方。
  • nitpick / thought / typo 等变体,各团队用法不同。

带标签的评论看起来是这样的:

nit: 这里用 const 会比 let 更清楚一些。

question: 这个列表为空时会怎么样?

blocker: 这条路径上的文件句柄没有被关闭,似乎泄漏了。

suggestion: 把这个条件提到 early return 里,嵌套会少一些。

praise: 这里用 early return 处理得很干净。

一个标签带来的差异是巨大的。

  1. 作者知道优先级。 blocker 和 nit 明确区分开后,作者能立刻判断先处理什么、什么可以凭自己的喜好决定。
  2. 评审者的语气变柔和。 「nit:」这几个字会自动传递「这是小事,不用有压力」的信号。
  3. 争论减少。 如果作者跳过一条标了 nit 的建议,没人会不高兴,因为大家事先就约定好了「这不是必须的」。

除了标签,Conventional Comments 还有一个可选的装置叫装饰符(decorator):用括号附加额外的上下文,比如 (non-blocking)。可以写成 suggestion (non-blocking): ...这样。不必刻意讲究形式,只要团队对「nit 可以忽略」有一个共同的理解,评审文化就会明显不同。

用提问代替命令

在传达同一个指摘的各种方式中,最有力的转变是把命令变成提问。 原因有两个:一个是人性层面的,一个是技术层面的。

先说人性层面的原因。命令会让作者产生防御心理。「这是错的(This is wrong)」这句话听起来像是判决。作者的反应要么是反驳,要么是退缩。相反,「x 为 null 时会怎么样?(What happens if x is null?)」这样的问题会让作者开始思考。探究取代了防御。

也有技术层面的原因。你有可能是错的。 评审者掌握的上下文往往比作者少。如果你断言「这是个 bug」,结果发现上游某处早已有防护代码,那评审者就会很尴尬。提问的形式天然地规避了这个风险,因为提问本身就含有「我可能漏看了什么」的谦逊。

把命令变成提问的几个例子:

  • 「这里漏了 null 检查。」→「这个值有没有可能以 null 的形式传进来?」
  • 「这里需要加锁。」→「如果多个线程同时访问这里,会发生什么?」
  • 「这条查询很慢。」→「当这张表增长到几百万行时,这条查询会走哪个索引?」
  • 「这段逻辑有问题。」→「能不能说明一下这个分支具体处理的是哪种情况?」

不过需要把握平衡。并不是所有话都需要包装成问题。 对于显而易见的事实(比如笔误、语法错误)或确凿无疑的 blocker,用提问的方式绕着说,反而会显得像是被动攻击,或者浪费时间。像「这个循环会漏掉最后一个元素(off-by-one)」这种确定无疑的事,直接说清楚更好。提问是在不确定、并且想引导作者思考时使用的工具。

还有一点,要区分真正的问题和假问题。 如果「你为什么这么做?」实际上是「这是错的」的委婉表达,作者很快就会看穿。这种伪装的批评,比坦率的指摘更糟。如果要提问,就要是真的想知道答案。如果你确信这是个真问题,就不要伪装成提问,而是明确标为 blocker。

小 PR — 大 PR 为何只能盖橡皮图章

决定评审质量的最强单一因素,出人意料地不是沟通技巧,而是 PR 的大小。 这直接关系到人类心理。

想象你收到一个 800 行的 PR。评审者会不自觉地感到压力山大。要老老实实逐行看完这么大一块代码,得花上一个多小时,可是手头还堆着别的工作。结果是可以预见的:评审者草草浏览一遍,点下「LGTM(看起来不错)」。这就是橡皮图章式评审(rubber-stamping)——评审只走了形式,没有发生实质性的审查。

研究和经验一致指向以下几点:

  • 大 PR 的缺陷发现率更低。 评审者的注意力是有限的,大 PR 会把这份注意力摊得很薄。一个常见的观察是,一旦超过几百行,缺陷发现率就会骤然下降。
  • 小 PR 能获得真正的评审。 一个 50 行的改动可以完整装进评审者的脑子里,让他们真正逐行思考。
  • 小 PR 合并得更快。 因为评审不构成负担,不会被搁置,反馈的往返也更快。

所以,好的作者会把改动拆分成小而内聚的单元。 理想情况下,一个 PR 只包含一个逻辑改动。不要把重构和新功能混在同一个 PR 里。一旦评审者不得不每次都去分辨「这一行是重构还是新行为」,评审就会变得急剧困难。

当大改动无法避免时,有几种策略:

  • 拆分为逻辑清晰的提交。 整理好历史记录,让评审者可以按提交逐个跟进。这种提交拆分和分支策略,在真实的仓库里练习是最快的掌握方式。在Git 练习场里熟悉拆分分支、重整提交的工作流,处理大改动时会轻松很多。
  • 堆叠式 PR(stacked PRs)。 把一个大功能拆成若干个存在依赖关系的小 PR,层层堆叠。每个 PR 都可以独立评审。
  • 纯粹的移动单独处理。 仅仅是移动代码(移动位置、改名)的改动,应该和逻辑改动分开,单独做成一个 PR,这样 diff 才能保持干净。

称赞做得好的代码

大多数评审指出问题。评审者会不自觉地进入猎手模式,专注寻找缺陷,而做得好的部分则被悄悄跳过。但这是一种被错过的机会,也是一种会侵蚀评审文化的习惯。

称赞之所以重要,有几个原因:

  1. 防止评审变成纯粹的负面体验。 十条指摘中间夹着一句真诚的称赞,作者就会把评审感受为一场对话,而不是一次攻击。这关乎心理安全感。
  2. 强化好的模式。 明确指出哪种做法很出色,作者和团队就会重复这种模式。评审不只是缺陷过滤器,更是学习的场所
  3. 建立信任。 当作者知道评审者会认可好的地方,他们也会更认真地对待这位评审者的批评。

称赞具体时才有力量。「做得好」是空洞的。「你提前处理了这个错误情况,真的很棒,救了以后调试这段代码的人」则说明了什么好、为什么好。

praise: 把这段递归改成循环写得很好,不用再担心栈溢出了。

praise: 谢谢你在测试里仔细补上了边界情况,评审起来很轻松。

praise: 这一条注释完美说明了为什么要这么写。谢谢。

需要注意的是,虚假的称赞反而会适得其反。 如果每个 PR 都机械地塞进一句称赞,它的分量就会消失。真正看到做得好的地方时,才发自内心地指出来。这份真诚才能让称赞保持效力。

优雅地接受反馈 — 作者的姿态

到目前为止,我们主要讨论的是评审者的态度。但评审是双向的。接受反馈一方的态度,同样左右着评审文化。老实说,这一面要难得多——因为当自己的代码被指摘时不产生防御反应,本身就是在对抗本能。

几条核心原则:

1. 把自我和代码分开。 这是最重要的一条。你的代码不是你。对代码的批评不是对作为一个人的你的批评。做不到这种分离,每次评审都会感觉像是人身攻击。反过来,一旦做到了这种分离,指摘就只是让代码变得更好的信息。工程师越资深,越会把对自己代码的批评看作免费的改进机会,而不是威胁。

2. 假定对方是善意的。 当评审者的评论显得生硬,或者语气不明确时,不要往最坏处想。大多数指摘都出自想让代码变好的善意。文字很容易丢失语气,评审者也可能只是忙,才写得简短。与其想「这个人是不是在轻视我?」,不如想「这个人在担心什么?」。

3. 感谢评审者。 评审是需要花时间和精力的工作。有人认真看了你的代码,这是一份礼物。尤其是当对方抓住了一个好 bug 时,要明确表达感谢。「好指摘,我差点漏掉了」这样一句话,对评审者来说意义重大。

4. 如果不同意就去对话,但要优雅地对话。 你不必接受每一条指摘。评审者也可能是错的,或者遗漏了上下文。这时,用解释而不是防御来回应。不要说「你错了」,而是说「这样理解也说得通,不过我是出于这些原因才这么写的,你觉得呢?」。如果真的无法达成一致,不要在评论串里无休止地争论,而是转到通话或面对面交流。文字会放大对立,而声音能让它缓和下来。

5. 回应每一条评论。 无论是采纳了指摘,还是决定保留原样,都在每条评论下留一句简短的回应。沉默会让评审者感到不安,觉得自己被无视了。「已修改」或「出于这个原因保持不变」这样一行字,就能干净地关闭这条讨论。

防御守护的是你的代码,而开放守护的是你的成长。

评审者的责任与作者的责任 — 一份双向契约

良好的评审文化不是靠一方的努力就能建立起来的。它是一份评审者与作者各自认真履行责任的双向契约。 只要有一方违约,另一方的努力也会随之崩塌。

把两个角色的责任并排放在一起,是这样的:

评审者的责任作者的责任
快速响应(不让评审被搁置)把 PR 做成便于评审的大小
把代码和人分开来批评接受反馈时把自我和代码分开
明确区分 blocker 和 nit在 PR 里写好说明(做了什么、为什么)
说明为什么提出这条指摘回应每一条评论
也要指出做得好的地方假定善意并表示感谢
区分个人喜好和真正的问题尊重评审者的时间(先自我评审)

这张表里有几点特别值得强调。

评审延迟是一种无声的毒药。 评审被搁置数日,作者就会丢失上下文,分支会变旧,合并冲突会累积,最重要的是,动力会被消磨殆尽。好的团队会把评审当作高优先级的工作来对待。「明天给一份完美的评审」通常不如「今天给一份足够好的评审」。要记住,一个评审请求就是正在阻塞别人进度的工作。

作者有责任减轻评审者的负担。 一份好的 PR 说明就是评审工作的一半。写清楚这个改动做了什么为什么需要它、是如何测试的、评审者应该特别留意的地方在哪里。而且在请求评审之前,先自己读一遍自己的 diff。 作者自己先抓出遗留的调试输出、被注释掉的代码、笔误,就能节省评审者的时间。自我评审是最起码的礼貌。

评审者有责任区分喜好和问题。 「换我就不会这么写」通常是喜好问题,而不是真正的问题。如果代码是正确、清晰的,不应该仅仅因为和评审者个人风格不同就要求修改。把精力花在真正重要的事情上(正确性、可读性、可维护性),喜好上的差异要么轻描淡写地标成 nit,要么干脆放过。

当这份契约被遵守时,评审就不再是关卡,而是协作——两个人各自信任对方的角色,朝着同一个目标努力。

结语

把代码评审看作沟通,就会发现大部分摩擦的根源不在技术,而在传达方式。瞄准代码而不是人,用「我们」代替「你」,用标签区分 nit 和 blocker,用提问代替命令,把改动拆小,称赞做得好的地方——所有这些技巧的共同点,是把对方当作协作者对待。

而且评审是双向的。作者有责任把自我和代码分开、假定善意、感谢评审者、做出便于评审的 PR。评审者有责任快速响应、说明理由、区分喜好与问题。当双方都遵守这份契约时,评审就不再是互相较量的场所,而是共同把代码变得更好的地方。

一份技术上完美的评审可能会破坏关系,一份友善却粗疏的评审可能会放过一个 bug。好的评审能同时兼顾这两者——既准确又友善,既严格又尊重对方。这正是不带摩擦的反馈的本质。

参考资料

현재 단락 (1/93)

表面上看,代码评审是一项技术活动。你在找 bug、验证设计、统一风格。但凡在团队里待过一段时间的人都知道:评审之所以会失败,往往不是因为技术,而是因为**人**。一个完全正确的指摘可能会破坏关系。一句...

작성 글자: 0원문 글자: 6,299작성 단락: 0/93