去杂乱:让生成的代码与细心的人类代码无异
杂乱不是一个风格问题,而是一个信任问题。一旦审查者发现一处“杂乱”,他们就会停止阅读代码,并开始四处寻找更多杂乱痕迹。修正方案绝不仅仅是表面功夫:下面的每一道工序都在验证代码的“必要性”,而非仅仅关注其外观。
基于实际的差异记录(diff)进行工作,永远不要依赖你编写时的记忆。如果工作跨越多个分支,请针对每个分支与其合并基准之间的差异进行审查。
工序 0 —— 小步提交,逐行证明(预防胜于清理)
在代码被写出来之前,去除杂乱的成本是最低的。在构建过程中,请遵循以下原则:
- 逐块构建。 从最小的、只做一件事的变更开始;在添加下一个部分之前,确保当前代码正确且符合语言习惯。一次性写就的大型 diff 总是会堆积杂乱;而 N 个小型增量(每个都对照本技能进行审查)则不会。这也会产生小而美的 PR,而审查者通常更信任这类 PR,其审批速度也比大 PR 更快。
- 在创建时审问每一行代码:这一行为什么存在?为什么这是一个新函数,而不是对现有函数的调用?为什么需要新文件?如果答案是“生成流程就是这样走的”,那么请删除或合并它。每一个新增内容都需要一个理由,并且这个理由在审查时你能够大声说出来。
- 不要过度复杂化。 在出现第二个调用者之前,明显的 5 行版本胜过抽象的 25 行版本。不要添加投机性参数,不要有“为了未来的灵活性”而预留的功能,不要为单个值包装类型。
- 不要重复造轮子 —— 这同样适用于编写阶段,而不仅仅是在清理阶段:在编写任何工具函数之前,先在仓库中搜索现有的辅助函数,并检查依赖树中是否有处理该问题的包(参见工序 2)。
- 在编写代码前,先匹配周边代码的风格。 阅读相邻的文件;采用它们的命名风格、错误处理和测试框架,这样新代码看起来就像是原本就存在的一样。
如果任务无法逐块构建(例如大型移植、供应商合同),请明确说明,并在其他人看到代码之前,通过更严格的工序 1 到 5 进行补偿性审查。
工序 1 —— 注释
这是最大的“杂乱”信号。应用一条规则:注释必须陈述代码无法表达的约束。 除此之外,全部删除。
看见即删:
- 叙述性内容:
// 循环遍历条目,// 调用辅助函数,// 返回结果 - 在函数体内重复自己的文档头部或代码本身
- 审查者式的自言自语:
// 这是安全的,因为我们上面检查过了,// 注意...—— 这是你在为差异记录辩护,而不是在告知下一位读者 - 引用 PR、审查、讨论或“最近更改过”的内容
- 解释变更为何正确的注释(应放在 PR 主体中)
- 在约 300 行以下的文件中的章节标题(
// ---- 辅助函数 ----)
保留(这些是承重结构):
- 线格式或协议约束,特别是那些通过逆向工程获得的、没有公开规范的:即使为空,字段 X 也必须存在;客户端 Y 会因缺少 Z 而拒绝整个负载;请引用上游源文件
- “为什么不”注释:解释为什么明显的方案被拒绝,以及什么会出错
- 读者可能会绊倒的平台事实:这个环境变量是 Unix 约定;这个 API 在 Windows 上静默地不执行任何操作
- 安全/数据安全的不变性规则:永远不要缓存原始记录,因为它嵌入了秘密
- 故意固定策略,与上游默认值不同,并附有触发条件
密度启发式规则(非强制):对于胶水/CRUD 代码,每 15 行新增代码中超过约 1 行注释,就需要仔细审视。对于无文档的协议代码,高密度可能是正确的 —— 注释本身就是规范。根据保留/删除列表来判断每条注释,永远不要只凭数量。
文档头部:共 4 到 8 行纯粹的约束。如果头部是用段落逐步叙述模块功能的,请将其缩减到文件名和导出内容无法表达的部分。
工序 2 —— 依赖或标准库已提供,但仍手动编写的代码
第二大“杂乱”信号:重复实现 uuid、哈希、语义化版本解析、全局匹配、日期计算、深度合并等。
步骤:
- 检查包已有的依赖项(读取 package.json,不要凭记忆)。优先使用现有依赖或运行时标准库,而非添加新依赖。
- 如果确实需要新依赖,选择经过实战检验的、无聊但可靠的库。
- 在替换现有的手动实现前,通过经验证明两者的等价性 —— 对相同的输入运行两者,并比较输出。尤其是确定性 ID 和哈希代码:一个改变输出的替换会静默地破坏现有用户的幂等性。
- 如果保留手动版本是正确选择(避免仅为 10 行代码增加一个依赖),请预判审查者可能提出的问题,并决定争论该问题是否比引入依赖更有价值。通常不是:无聊的选择在审查中胜出。
- 例外:已经是标准库组合的代码(
createHash+pipeline)不算手动实现。如果有疑问,这样说明即可;不要进一步包装它。
工序 3 —— 内聚性与必要性
杂乱是逐渐积累的:每次编辑都会增加内容,而不会整合。寻找以下内容:
- 重复的条件表达式 → 提取一次,按含义命名(
noConfigChanges,而不是check1) - 相同字面量出现在 N 个文件中 → 在拥有该概念的模块中创建一个共享常量;其他模块导入它
- 仓库中已有辅助函数的本地重复实现 → 在保留私有副本之前,先搜索现有的辅助函数(
pathExists、输出/格式化工具、错误辅助函数)。删除本地副本,导入共享版本。 - 死代码:对于差异记录中的每个辅助函数/类型/导出,找到一个使用者。没有使用者 → 立即删除,不要“留待以后”。
- 调用点手动实现本应由辅助函数处理的功能(如空格、引号、ID 生成) → 将行为移入辅助函数,精简调用点
- 重复陈述同一事实(如文档头部 + 内联注释) → 保留一处
- 返回松散相关值元组的函数 → 通常是两个职责被融合的标志;进行拆分或记录合同
然后检查与仓库的契合度:阅读相邻文件,匹配它们的习语、命名、错误风格和测试模式。忽略团队风格指南的代码,即使正确,也看起来像是生成的。
工序 4 —— 测试
- 测试必须固定行为,而不是固定 Bug。如果一个断言编码了错误的值“因为代码就是这样做的”,则同时修复两者。
- 反映测试套件中已有的框架和辅助函数;不要在同一文件中发明并行的 mock 风格。
- 每个平台特定的跳过必须附带一行理由。
- 测试名称描述场景,而不是实现。
- 当你更改输出结构时,针对已删除的格式添加一个否定断言。
工序 5 —— 工具链一致性与最终验证
- 使用仓库固定的格式化器/linter 版本(读取 package.json —— 在本地运行不同的主版本会导致只在 CI 中失败的 Bug)。
- 在堆叠变更的每一层,运行差异记录所触及的测试,而不仅仅是最终那一层。
- 在所有编辑完成后,再次审查最终的差异记录:运行
git diff并逐条大声朗读所有剩余的注释,对照工序 1 的标准。检查密度:git diff | grep -cE '^\+\s*(//|\*)'与总新增行数对比。 - 提交信息:用散文的形式解释“为什么”,像一个正常人那样。对于 3 行代码的提交,不要使用列表式的变更日志,也不要用“增强/改进/鲁棒”这类词语。
报告
当被问及“这个代码去杂乱了吗?”:审查当前的差异记录,并附上证据进行回答(密度数字、删除了什么、主动保留了什么及原因),永远不要仅凭记忆回答“是”。如果保留了某个审查者可能会标记的内容——例如一个高密度的协议文件——请主动命名它,并用一句话给出辩护。
当审查者称某些内容为“杂乱”时:在接受任何具体实例之前,先承认这种模式的存在。在全部范围内修复该类别,包括审查者未标记的文件(以及他们表扬过的文件)。将标准当作一门纪律来应用,而不是对标记行的亡羊补牢,这才是重建信任的关键。