从症状到重构手法
评审意见写着「这段代码不够好,耦合太高」。作者问怎么改,评审者也说不出具体的一步。几轮讨论之后,代码被拆成了七个类,读起来比原来更难,因为每个类都只做一点点,要理解一个流程得在七个文件之间跳。
「代码不美」「耦合太高」这类评价很难变成行动。能变成行动的,是具体的信号和具体的手法:这个方法的名字说不出它做了什么,这两段代码一样,这个参数列表有七个参数。每一个信号背后可能是不同的问题,同一个信号,答案不同,手法可能正好相反。
这一篇是专题的总览:把常见的信号、要先问的问题、可能的手法和改动时的风险放在一起,再指向展开讨论的文章。
一、信号不是结论
代码坏味道(code smell)这个说法本身就说明了它的性质:闻到了味道,要去查一查是什么,而不是一闻到就动手。几个容易被当成结论的信号:
- 方法长。 长度只是提示。一个状态转换表有三十个分支,硬拆开只会让规则散落各处。真正要问的是:它是否混合了不同层次的事情。
- 有 setter。 数据传输对象、配置对象、框架绑定的对象,有 setter 是正常的。要问的是:这个对象有没有需要守住的规则,setter 是否让外部绕过了规则。
- 有
else、有switch。 它们是表达规则的基本手段。要问的是:同一个分派是否出现在多处。 - 类名带 Manager、Helper。 有时是职责不清的信号,有时就是准确的名字(
TransactionManager)。
把信号当成结论,就会出现「禁止 else」「方法不超过十行」「每个类最多两个字段」这类规则。它们作为训练时的约束有价值(逼着人去想别的写法),作为生产代码的标准,会制造出更多问题。
二、信号、问题与手法
| 信号 | 先问 | 可能的手法 | 展开 |
|---|---|---|---|
| 名字说不清在做什么 | 它在业务上是哪一步?是不是装了几件事? | 改名;先拆开再分别命名 | 命名与改名 |
| 两段代码一样 | 需求变了,它们一定一起改吗? | 是:合并成一个来源;否:保持分开、各自命名 | 重复的知识与偶然的相似 |
| 方法很长 | 能写出它的几个业务步骤吗? | 按步骤提取方法,保持原有顺序 | 保行为的拆分 |
| 参数很多 | 这些参数为什么一起出现? | 参数对象、构造器注入、按意图拆成多个方法 | 参数放在哪里 |
| 嵌套很深 | 主流程是什么,哪些是特殊情况? | 卫语句、提取单个元素的处理 | 控制流与复杂度 |
| 同一个 switch 出现在多处 | 新增一个类别要改几处? | 多态;只出现一次时用 switch 表达式 | 控制流与复杂度 |
| 变量先给初值、后面零散修改 | 这个值在哪一刻才算准备好? | 一次性计算;final 加确定赋值 | 局部状态与集合的契约 |
| 外部直接改对象内部状态 | 谁应该守住这条规则? | 用业务方法代替 setter;不交出内部集合 | 不变量的入口 |
| 调用方沿着对象链一路取数据 | 调用方真正要的是什么? | 把行为移到拥有数据的对象上 | 封装、组合与多态 |
| 一层只是原样转发 | 这一层还有存在的理由吗? | 内联,去掉中间人 | |
| 一个类因为几种不同的原因被修改 | 修改它的人和原因有几类? | 按变化原因拆分 | 设计原则 |
最后两行说明了一个常被忽略的事实:重构手法是成对的。提取方法与内联方法、提取类与内联类、隐藏委托与移除中间人,方向相反。过度提取的代码同样需要重构,手法是把它们合回去。开头那个「拆成七个类」的例子,需要的正是内联。
三、每一步都要能证明行为没变
重构的定义是不改变外部可观察行为的结构调整。「不改变行为」不能靠感觉,要有证据:
- 自动化测试,覆盖要改的代码,并且断言足够强(只检查「没有抛异常」的测试保护不了什么,见覆盖率 100%,一个断言都没有)。
- 字符化比较:没有足够的测试时,以原实现的输出为准,用大量输入比较新旧实现。
- IDE 的自动重构:提取、内联、改名、移动,在编译器看得到的范围内是保行为的。编译器看不到的地方(反射、序列化、配置、模板)需要另外检查。
改动要小。一次只做一种手法,做完立即验证,验证通过就可以提交。这样每一步之后都可以停下来去做别的事,代码始终处于正确的状态;出了问题也只需要回退最后一小步。
四、什么时候不重构
- 没有保护的时候。 没有测试、也来不及建字符化比较的代码,先建保护,再重构。
- 没有理由的时候。 重构服务于接下来的修改:要加一个功能,先把代码整理成容易加的样子,再加。与当前工作无关的「顺手重构」,会让变更难以评审,也增加了风险。
- 与行为变更混在一起的时候。 一个提交里既重构又改行为,出了问题分不清是哪一个引起的。分开提交,先重构、后改行为。
- 对外契约。 公开 API、序列化格式、数据库结构的改动不是重构,是契约变更,要走兼容流程。
参考资料