40 重构

在测试保护下持续重构,以小步、行为等价和可回退提交改善设计而不夹带功能变化。

学习目标

  • 能识别重复、过长责任链和错误依赖方向,并先写出外部行为合同
  • 能在测试保护下用单一、小步、行为等价的变换改善设计
  • 能用提交边界、回退动作和失败记录证明重构没有夹带功能变化

重构是受保护的设计实验

本页依据 David Thomas、Andrew Hunt《程序员修炼之道:通向务实的最高境界(第2版)》,云风译,电子工业出版社,2020 年 4 月,ISBN 9787121384356 的公开中文目录,独立重构 40 重构。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图或答案。

重构不是“把代码改得更漂亮”,也不是一次顺手加入新功能。它是在外部行为保持不变的前提下调整内部结构,让责任、名称、依赖方向或副作用边界更清楚。每一次变化都应该像一个小实验:先有保护证据,再做单一变换,验证后留下可回退提交。

三个会让重构失去因果的陷阱

从坏味道到可回退提交

<Term def="提示责任、依赖方向、重复或副作用边界可能失真的结构信号,而不是自动决定如何修改的规则。">坏味道</Term>只是调查入口。重构回路要把它连接到行为证据和提交边界:

  1. 识别坏味道:指出重复、过长函数、散落条件或错误依赖的具体位置。
  2. 保护行为:记录正常、边界、错误和副作用场景,先让测试表达外部合同。
  3. 做小变换:一次只提取、重命名、移动责任或隔离副作用中的一项。
  4. 验证等价:运行快速测试和端到端切片,比较输出、错误和副作用。
  5. 提交与回退:提交只包含这一变换;失败时回退提交并用同一输入重放。
重构:用小步保持行为,再改善结构每次只改变一种结构因素,失败时回到最近一次已验证状态1坏味道指出结构信号已留证据2保护测试锁定外部合同已留证据3小变换只改一种因素当前变换入口4验证比较行为与副作用等待证据5提交可独立回退等待证据绿色验证不是终点:提交边界和回退路径也必须可复核
专属图示:重构把结构变化切成可验证、可提交、可回退的小实验。

40 与提示 65:尽早重构,经常重构

本单元对应中文目录的 40 重构提示 65:尽早重构,经常重构。尽早不表示在需求尚未理解时过度设计,经常也不表示无止境地追求抽象;它强调在阻力仍小、行为仍可观察时用小步改善结构。

例如,一个订单函数同时解析输入、计算折扣、写数据库和发送通知。第一次重构可以只提取纯折扣计算,并用已有端到端场景保护订单结果;下一次再隔离通知副作用。若一次把四项全部改写,测试即使最后通过,也很难知道通知重复或折扣变化在哪一步发生。

<Term def="调用者可观察的输入、输出、错误、时序和副作用规则;重构必须默认保持这组规则。">行为合同</Term>要先于内部设计。合同不只包括成功结果,也包括拒绝、超时、幂等、日志和事务边界。若合同尚未明确,先补观察和测试,不能用重构掩盖未知。

小步变换的工作方式

<Term def="只改变一种结构因素、完成后可以单独验证和回退的最小重构动作。">小步变换</Term>不是按代码行数衡量,而是按因果边界衡量。重命名一个概念可能影响许多文件却仍是单一变换;同时改名、改变默认行为和替换数据库则不是。

type LineItem = { price: number; quantity: number };
 
function subtotal(items: ReadonlyArray<LineItem>): number {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
 
function applyDiscount(total: number, rate: number): number {
  if (rate < 0 || rate > 1) throw new Error("discount-rate-out-of-range");
  return total * (1 - rate);
}

先把纯计算从副作用流程中分离,测试可以直接表达金额和折扣的行为。之后才有条件决定是否替换金额类型、移动边界或调整展示格式;每一步都保持可观察结果明确。

<Term def="允许恢复到最近一次已验证状态的提交、补偿或操作路径;它必须在改动前就可执行。">回退路径</Term>是重构的一部分,不是失败后的临时补丁。记录回退命令、数据副作用是否可逆、需要谁批准以及回退后要重跑哪些测试。若数据库迁移不可逆,先把它从纯结构重构中拆出来。

练习实验:让提交保持单一原因

Interactive lab

选择证据样本,决定提交还是回退

边界合同

输入证据

空值、超时和拒绝路径需要在重构前先固定。

实际首差

边界节点:测试暴露了原本未声明的行为。

恢复动作

先补合同测试,再重新选择小变换。

先写保持的行为,再选择样本;失败时回退到已验证状态,不要在混合工作区继续猜测。

分步1 / 4

1. 写出坏味道与行为合同

指出一个具体重复或责任混合点,列出调用者可观察的成功、边界、错误和副作用。若合同还不清楚,先添加观察测试而不是移动代码。

正常、边界与单一故障证据

证据矩阵:行为合同比最终绿色更重要正常、边界和故障样本共同证明重构没有夹带功能变化字段正常边界故障合同输出保持错误保持副作用重复变换单一因素拒绝可见测试失败首差边界节点验证节点恢复提交哈希补测试回退重放失败时保留最小输入、首个差异、回退提交和未覆盖行为
专属图示:重构的质量由行为等价、失败可见和回退可执行共同定义。
样本唯一变化预期判定必存证据
正常一种行为等价变换测试与外部结果保持一致变换、输出和提交哈希
边界错误、超时、空值或副作用合同不变,拒绝仍可观察边界输入、错误和事件数
单一故障一个测试、依赖或回退步骤失效在首个差异处停止失败记录、回退和重放

独立复核者应能从提交和测试判断这次变化只做了一件事。若提交说明写“清理并优化并修复”,先拆分历史;若失败后只保留最终绿色状态,说明证据链已经丢失。

什么时候不该重构

当行为合同完全未知、外部依赖不可观测、数据迁移不可逆或发布窗口无法提供回退时,先补观测和恢复能力。重构不是逃离需求不确定性的方式;可以先做小型探针或记录,等边界清晰后再改变结构。

自动化工具可以批量重命名、提取函数或更新调用点,但每批变化仍要有单一原因、行为验证和回退提交。工具完成得很快,不代表风险变小;真正的验收是复核者能说出“改了什么、保持了什么、失败如何回到哪里”。

术语表

名词解释

本章出现的专业名词,用大白话再讲一遍。

坏味道

提示责任、依赖、重复或副作用边界可能失真的结构信号。

行为合同

调用者可观察的输入、输出、错误、时序和副作用规则。

小步变换

只改变一种结构因素、可以单独验证和回退的重构动作。

回退路径

允许恢复到最近一次已验证状态的提交、补偿或操作方式。

行为等价

重构前后外部可观察结果和拒绝条件保持一致。

练习

练习

问题 1: 一个函数同时读文件、解析内容和发送通知。你会如何拆第一步重构?

问题 2: 重命名一个字段后,有一个旧客户端开始报错。这说明重构失败了吗?

问题 3: 重构后单元测试通过,但通知数量翻倍。你如何回退和定位?

资料与写作方式声明

本章以程序员修炼之道权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

本单元回顾

重构是受保护的设计实验:先识别坏味道,写出行为合同,用单一小步改善结构,验证外部结果,再提交或回退。持续频繁并不意味着随意扩大范围,而是让每次变化都小到可以理解、复核和恢复,避免结构债务与功能变更在同一提交中纠缠。

前后导航

讨论

评论区加载中…