2330 字
约 7 分钟
13
S24-A27 · 可审性:为什么"多改三个文件"让这单代码没人敢合

需求本身很小:把某配置的默认值从 30 改成 60,顺便"它发现这里有个 typo"。AI 提交上来 830 行 diff、14 个文件,其中 9 个文件是"顺手统一了格式与 import 顺序"。评审挂了三天,最后的结果是——没人真的看过那行关键的 60。

可审性(reviewability)不是代码质量的子项,它是独立的一笔账:一份 diff 如果超出 reviewer 的认知预算,它就等于没被审。而 AI 特别容易把 diff 撑大,因为它没有"这次只管这一件事"的边界感。


1. review 的认知预算是有上限的

规模 实际发生什么 结果
< 100 行、1–3 文件 逐行读 能发现问题
100–400 行 重点区域精读、其余扫过 大概率漏边界
400–800 行 只看 diff 摘要与关键函数 形式化通过
> 800 行 或 > 10 文件 看标题与 CI 结果 等于没审

第三行是"LGTM 文化"的物理来源:不是 reviewer 不负责,是人眼在 400 行之后的注意力断崖。而这个上限与代码难度无关——再资深的人也一样。

// 一次 830 行 diff 的实际构成(真实形状)
核心改动       12 行   1.4%   ← 只改了默认值 + 一行注释
格式化/import 610 行  73.5%   ← 顺手统一
重命名          95 行  11.4%
顺带重构       113 行  13.6%
// 结论:reviewer 要在 98.6% 的噪音里找那 12 行

2. 三类噪音,破坏方式不同

噪音类型 为什么破坏 review 是否可机械识别
格式化(空格/引号/import 顺序) 让真实改动淹没在白色噪音里 可以(.git-blame-ignore-revs)
跨文件重命名 打断"改动意图"的连续性 部分(rename detection)
顺带重构(抽方法、改结构) 改变了行为假设,却混在功能改动里 不能

第三行最危险:格式化至少"语义不变",而顺带重构会引入新行为。当它和功能改动混在一个 diff 里,reviewer 无法区分"这行是为了实现需求"还是"这是他顺手改的"——于是要么全信,要么全不信。


3. 判据:核心改动占比与可独立回滚性

不要用"行数"作为唯一判据,用两个更硬的:

判据 阈值 说明
核心改动占比 ≥ 30 % 低于此说明噪音太多
文件数 ≤ 5(业务改动) 超过就要说明为什么
可独立回滚 是 只回滚功能部分,重构留下
单一意图 是 一句话能说清"这单做了什么"
// 噪音比:两个数字一除,就知道这单该不该拆
$ total=$(git diff --numstat HEAD~1 | awk '{s+=$1+$2} END{print s}')
$ sem=$(git diff -w --numstat HEAD~1 | awk '{s+=$1+$2} END{print s}')
$ echo "total=$total semantic=$sem"
// total=830 semantic=108 → 核心占比 13%,低于 30% 就该拆单

第三行常被忽略,但它才是关键:如果回滚这单会把无关的重构一起撤掉,那这单就不该存在。可独立回滚要求把"行为不变"的改动与"行为变化"的改动分成两个提交。

// 提交前自查:这单到底改了多少、噪音多少
$ git diff --stat HEAD~1 | tail -3
 9 files changed, 812 insertions(+), 18 deletions(-)
$ git diff -w --stat HEAD~1 | tail -1        // 忽略空白后的真实规模
 5 files changed, 96 insertions(+), 12 deletions(-)
// -w 之后的数字才是"语义改动"的规模;两者差 8 倍就是明确的噪音信号

4. 拆分:两段式提交,而不是一个巨型 PR

阶段 内容 期望 diff 回滚含义
1 纯重构 / 格式化 行为不变 可以很大 回滚无风险
2 功能改动 行为变化 应该很小 回滚即恢复旧行为
3 独立提交说明 每段一句话 — 便于 bisect

第一段可以被快速批准(因为它理论上不改行为,且可以用"测试全绿"作为证据),第二段才需要认真审。这个顺序不能颠倒——先功能后格式化,会让功能 diff 也被迫放在噪音里。

// 一个可执行的门禁(放在 CI 里,不靠自觉)
review_gate:
  max_files: 5
  max_semantic_lines: 300        // git diff -w 之后的行数
  core_ratio_min: 0.3            // 核心改动 / 总改动
  require_split_when:
    - rename_count > 0 && feature_change == true
    - reformat_count > 200 && feature_change == true
  // 触发即失败,提示"请拆成两单",而不是警告

5. 什么时候"顺手改"是对的

不是所有附带改动都该拆。判断标准是"它是否会改变别人的行为假设"。

场景 可以顺手改 理由
同一个文件内的 typo ✅ 零风险、零认知负担
新代码里的 import 顺序 ✅ 新文件无历史包袱
被改函数内的局部变量重命名 ✅ 作用域封闭
相邻文件的格式化 ❌ 制造跨文件噪音
顺手抽个公共方法 ❌ 引入新行为 + 影响其他调用方
顺手升级依赖 ❌ 完全独立的风险面

第五行是最常见的"善意破坏":抽公共方法看起来是改进,但它改变了调用链,而这个改变没有对应的需求与测试。真要抽,另开一单,让它有自己的评审与回滚路径。


6. 排错:五种"review 失效"的现场

现象 八成原因 处理方向
PR 挂着没人看 diff 太大,无人愿意开始 拆成 ≤ 400 行
合了之后才发现 bug 核心改动被噪音淹没 用 -w 重看 + 拆单
回滚把重构也撤了 两类改动混在一个提交 两段式提交
reviewer 只留 LGTM 认知预算已耗尽 缩小 diff,而不是要求更认真
同一个文件反复被改 每次都在"顺手统一" 一次性格式化 + ignore-revs
// 同样的 diff,两种读法,结果差一个 bug
A 逐行读 830 行(从头到尾)      → 120 行后注意力下降,漏掉核心那 12 行
B 先看 git diff -w 的 96 行 → 再看文件结构 → 最后核对格式化
// 成本差一分钟;前者是"尽力了但没看见",后者是"看见了"

第二行给出一个很实用的动作:评审时先看 git diff -w(忽略空白),把噪音过滤掉,再决定要不要逐行。这个动作能把 830 行降到 96 行,成本是一秒钟。


7. 边界:什么情况不该拆

场景 为什么不该拆 说明
线上故障热修 拆分会拉长恢复时间 允许大 diff,事后补拆
原子性的迁移 中间态不可运行 必须一次完成
生成代码(protobuf 等) 拆分无意义 标注"generated"
一次性脚本/工具 无长期维护 不必强求
单人项目 没有 review 成本 按自己的判断

判断口径一句话:可审性是为"别人要读懂并承担风险"付的成本——只要存在第二个要 review 或要维护的人,这单就该守 400 行与 5 文件;而当恢复时间比评审更重要(热修),允许例外的同时要在事后把例外补成常规,否则例外会变成习惯。


8. 自测

  1. review 的认知预算为什么会在 400 行之后断崖?它与代码难度有关吗?
  2. 三类噪音里,哪一类"语义不变"、哪一类"会引入新行为"?为什么后者更危险?
  3. 判据里"可独立回滚"具体指什么?为什么它比行数更硬?
  4. 两段式提交的顺序为什么不能颠倒?
  5. 什么情况"顺手改"是合理的?判据是什么?
  6. 线上热修时允许大 diff,事后必须补什么?

9. 一句话总结

可审性是独立的一笔账:超出认知预算的 diff 等于没被审。噪音(格式化/重命名/顺带重构)的真实代价不是行数,而是把核心改动埋掉、并让回滚失去边界。三条硬判据——核心占比 ≥ 30 %、git diff -w 后 ≤ 300 行、可独立回滚;做法是两段式提交(先行为不变、再行为变化)。判据的边界也很清楚:有人要读懂并承担风险时守这个规矩;恢复时间优先时允许例外,但事后必须补成常规。

S24-A27 · 可审性:为什么"多改三个文件"让这单代码没人敢合
https://www.clxhxhhr.top/posts/6738/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。