第 7 章 · 代码质量与重构

代码是写给两个人看的:机器(编译器,它不在乎)和六个月后回来的你(它非常在乎)。本章讲代码质量的操作性定义(坏味道清单)、重构的纪律(小步 + 测试保护网)、以及命名与函数这两项日常功。重构(Refactoring,Fowler)的定义先立住:在不改变可观察行为的前提下,改善代码的内部结构。「不改行为」是红线,「内部结构」是全部内容。

7.1 坏味道:需要重构的信号

坏味道(code smell)是「该重构了」的嗅觉信号,不是错误(编译器全放行),但每一条都在积累未来的成本:

坏味道症状常用药方
重复代码同样逻辑出现 ≥ 2 处,改一处漏一处提取函数/类(第 3 条 DRY)
过长函数一屏装不下,注释分隔段落提取函数(按「一个抽象层级」切)
巨型类/上帝类一个类 2000 行、管 5 件事按职责拆(单一职责,第 5 章 SRP)
过长参数列表8 个参数,调用处靠注释对位引入参数对象
发散式变化一个需求改动横跨 5 个文件按变化方向重新切模块(第 4 章)
注释解释代码代码要靠注释才看得懂改代码,注释只留「为什么」
霰弹式修改一个小改动牵出 20 处小改内聚化(发散式变化的反面)

坏味道清单的正确用法:不是背下来,而是在 code review 和写代码时的「膈应感」上找到学术名字,然后查药方。Fowler 的《重构》第二版有完整目录,把其中「长函数」和「重复代码」两条治好,代码质量已能超过多数课程项目。

7.2 重构的纪律:小步 + 保护网

重构是把手术,两条纪律缺一不可:

小步。每次重构一个动作(提取函数、改名、搬移方法),动作之间跑测试、提交。大重构(一次性改 2000 行再测)出问题时,你不知道哪一步引入的。「小步快跑」在重构语境的含义:每一步都可回滚。

保护网。重构前先有测试(第 8 章),且测试针对行为而不是实现(测「存款 100 后余额为 100」,不测「调用了 save 方法」)。行为测试在重构中必须全绿;测试红了说明你改了行为,要么改错了,要么那是「功能修改」不是重构。没有测试的重构是闭眼开车,先补测试(哪怕是粗粒度的特征测试)再动刀。

IDE(IDEA/VSCode)的重构功能(重命名、提取函数、内联、移动)值得全学会:它们做的是语法级安全的变换,比手改可靠一个量级。

7.3 命名:最便宜也最贵的改进

命名是代码可读性的第一杠杆。几个硬规则:

  • 说清意图,不复述类型:daysSinceEpoch 优于 d;userList 优于 list2;
  • 避免缩写谜语:usrPwdExpChk 只有作者活着时能看懂;
  • 布尔名用 is/has/can:isEmpty、hasPermission;
  • 长度与作用域匹配:循环里的 i 合法,跨文件的 threeLetterName 不合法;
  • 注释只写「为什么」:代码写「做什么」,注释解释的是约束、坑、出处(「这里必须先锁 B 再锁 A,见 issue #41」),「// 设置变量」式的注释是噪音,删。

命名的终极检验:给同事(或三个月后的自己)看函数名,他能猜出大致行为,就及格了。「程序员最难的两件事:缓存失效和命名」这个段子,一半是真的。

7.4 函数:单一抽象层级

好的函数有一条可操作的判据:函数体内的语句都在同一个抽象层级(先讲「读文件、解析、过滤」,再在某层展开「解析」怎么做)。混合层级(一行调高级 API、下一行手写字节操作)是可读性杀手。配套规则:函数短(一屏内,可能十几行)、参数少(三个以上考虑参数对象)、无副作用默认(改外部状态要显式)。

还有「early return」风格:

// 嵌套地狱版
if (user != null) { if (user.isActive()) { if (user.hasPermission()) { ... } } }
// 卫语句版
if (user == null) return;
if (!user.isActive()) return;
if (!user.hasPermission()) return;
...主逻辑不缩进

卫语句(guard clause)先排除异常,主路径保持平铺,缩进层级是复杂度的直接读数。

7.5 技术债:诚实的会计

技术债(technical debt,Cunningham 的比喻)是「为了短期交付选择次优实现,欠下的未来成本」。债务不是罪(赶 deadline 的合理杠杆),失控才是。管理它的姿势:

  • 显式化:债要有记录(代码里 TODO 带上下文/issue 单),「没人知道的债」是最危险的债;
  • 算利息:每次在该模块加功能的额外耗时就是利息,利息超过本金重构时还债;
  • 童子军军规:每次提交让代码比来时干净一点(小步偿还,不做大扫除);
  • 定期还债:给重构排真实的优先级(backlog 里放「重构卡」,和功能卡平等竞争)。

常见错误

  • 「以后再重构」永远不来。债滚债,最后「这个模块谁都不敢动」;把重构成本计入任务估算才是诚实。
  • 重构和功能混在一个提交。review 者分不清哪改动是行为、哪是搬家;重构单独提交,信息写 refactoring。
  • 测试挂了硬重构。保护网破了还开车;先修测试(或确认测试本身错了,先修测试)。
  • 过度设计当重构。为「未来可能的需求」抽三层接口(第 5 章 YAGNI);重构是让当前代码更清晰,不是为想象中的明天盖楼。

练习

  1. 动手:翻出你写过的最长的一个函数(或同学的项目),按 7.4 的「单一抽象层级」逐段提取函数,重构前后各统计:行数、缩进层级、能看懂的同事人数(找一位同学实测)。
  2. 动手:给一个没有测试的旧模块补「特征测试」:跑几组输入,把现在的输出(哪怕它可能是错的)钉成快照,然后在保护网下做一次重构,验证快照不变。
  3. 手算:你项目里重复出现 3 次的一段 20 行逻辑,每次需求变更要改 3 处(每次 0.5 人天)。提取函数花 1 人天,之后每变更 1 处。算「第几次变更后回本」,再决定这个重构值不值得现在做。
  4. 思考:第 5 章 YAGNI(不做没人要的抽象)和本章「持续重构」(为可读性投资)之间的张力:一个说「别为未来建」,一个说「为未来维护性投资」。用「结构投资 vs 行为投资」的区分消解这个矛盾。