CH.01📚 书籍元信息
- 书名:《修改代码的艺术》(Working Effectively with Legacy Code)
- 作者:Michael C. Feathers
- 类型:软件工程 / 代码维护
- 输入类型:仅书名
- 一句话总结:这本书回答了如何在没有测试保护的遗留代码中安全修改的问题,答案是通过「接缝」定位和「依赖破坏」技术,系统性地建立测试安全网。
- 适读人群:需要修改无测试覆盖代码的开发者、接手他人项目的工程师、技术债务管理者;已完成全量测试覆盖的纯架构研究者可能觉得实操部分过于具体。
CH.02🔍 真问题
核心问题:当你面对一个没有测试、结构混乱、依赖纠缠的代码库时,如何安全地添加功能、修复缺陷,而不引发级联崩溃?这不是"如何写好代码"的问题,而是"如何在已经糟糕的代码中不把事情搞得更糟"的问题。
旧答案:主流做法有三类——①绕着走,尽量不碰;②直接改,凭经验和祈祷;③推倒重写(rewrite)。绕着走导致技术债务累积;直接改往往引入新 Bug;重写则成本极高且失败率居高不下。三者的共同缺陷是没有系统性的安全策略。
新答案:Feathers 提出的路径是:不急着改业务逻辑,而是先找到接缝(Seams),通过依赖破坏技术在接缝处插入测试,让原本不可测试的代码变得可测试;有了测试安全网之后,再做安全的增量修改。这是一套"先加固地基再施工"的系统方法论。
答案的底层逻辑:遗留代码的危险性本质上来自信息不对称——你不了解代码的真实行为,而代码的行为可能与其表面意图不一致。测试的作用是将隐式行为转化为显式文档。一旦你用测试锁定了当前行为(包括 Bug),任何修改都会被测试即时反馈,风险从"未知的未知"变成"已知的已知"。
关键边界:①当代码依赖于无法在测试中模拟的外部系统(如特定硬件、实时物理信号)时,依赖破坏技术的成本会急剧上升;②当团队缺乏基本的重构能力时,仅靠此方法可能陷入"为了测试而测试"的陷阱;③对于已经无法运行的代码(编译都过不了),需要先恢复可编译状态才能启动此流程。
CH.03🗺️ 知识地图
(图说明:本书从接缝这一核心概念出发,向外辐射出依赖破坏技术、测试安全网、修改策略三大分支,构成完整的遗留代码修改方法体系。)
CH.04💡 核心模型深度解析
模型一:接缝模型(Seams)
模型定义 在代码中找到行为可以被替换或观察的断点,使得你可以在不修改原始代码逻辑的前提下,改变程序的运行路径或验证其行为——这些断点就是「接缝」。
(图说明:接缝是代码中行为可被接管的断点,不同语言机制提供不同类型的接缝。)
原书论证 Feathers 按编程语言机制将接缝分为三类——函数接缝(运行时通过多态或模块替换接管函数行为)、对象接缝(运行时通过 mock/stub 替换对象)、预处理器接缝(编译时通过条件编译替换代码段)。书中强调,每种语言都有接缝,只是有些需要更多创造性才能发现。例如 C 语言中可以通过宏重定义和链接时替换创建接缝,Java 中可以通过接口和反射创建接缝。核心论点是:接缝是所有依赖破坏技术的物理基础。
迁移场景
- 系统集成测试:在微服务架构中,服务之间的调用点就是天然接缝。找到这些接缝,用契约测试(Contract Testing)替换真实调用,可以在不部署下游服务的情况下验证上游逻辑。
- 遗留数据库迁移:数据库访问层是接缝。在接缝处引入仓储模式(Repository),可以在不修改业务代码的前提下切换底层数据库实现。
- 游戏开发调试:游戏引擎的渲染接口是接缝。在接缝处插入调试渲染器,可以在不改动游戏逻辑的前提下观察游戏内部状态。
失效边界
- 隐式接缝:当代码中的依赖是隐式的(如通过全局状态、静态变量传递行为)时,接缝虽然存在但极难定位,定位成本可能超过收益。
- 紧耦合的模板元编程:当代码大量使用模板/泛型特化时,接缝在编译期就已固化,运行时无法替换。
- 反例:某些嵌入式系统中,硬件寄存器操作直接内联在业务逻辑中,物理上不存在可替换的断点。
改造方法
- 补充变量:引入「接缝发现」的前置步骤——先扫描代码的编译依赖图,列出所有可能的接缝候选点,按替换成本排序。
- 改造后形式:
扫描依赖图 → 标记接缝候选 → 评估替换成本 → 选择最低成本接缝 → 执行依赖破坏
行动接口(3 套 SOP)
🟢 小白版 SOP
- 触发条件:接手一个没有测试的模块,需要理解或修改其中的行为。
- 执行步骤:
- 列出该模块的所有外部依赖(函数调用、对象引用、全局变量访问)。
- 对每个依赖,问自己:"这个依赖的实现在运行时能否被替换?"——能替换的点就是接缝。
- 从替换成本最低的接缝开始,用最简单的替换技术(如提取接口 + 传入参数)创建第一个可测试点。
- 验证标准:替换后原功能行为不变(通过手动验证或特征测试确认)。
- 回滚机制:保留原始实现的备份分支,如果替换导致功能异常,立即回退。
🟡 老手版 SOP
- 触发条件:已掌握基础接缝识别,需要在复杂依赖网络中找到最优修改路径。
- 执行步骤:
- 构建完整依赖图,标注每个依赖的「修改风险值」(被多少模块依赖 × 依赖深度)。
- 识别「接缝密度高」的区域——多个依赖汇聚的类/函数,这里是投入产出比最高的测试点。
- 按风险值从高到低排序,在每个高风险接缝处建立测试,逐步扩大安全网覆盖范围。
- 验证标准:依赖图中的高风险节点测试覆盖率达到 80% 以上。
- 常见进阶陷阱:过度追求接缝覆盖率而忽略了真正需要修改的代码——测试应该服务于修改目标,而不是为了覆盖率。
🔵 团队版 SOP
- 触发条件:团队决定系统性地治理遗留代码的技术债务。
- 角色 × 步骤矩阵:
- 架构师:构建全局依赖图,划分修改域,确定优先级。
- 每个修改域的负责人:在指定接缝处建立测试。
- 测试负责人:验证测试质量,确认特征测试覆盖了关键路径。
- 验证标准:每完成一个修改域,该域的回归测试通过率 ≥ 95%。
- 回滚机制:按修改域粒度回滚,不跨域合并。
决策检查清单
- 是否已完整列出所有外部依赖?
- 每个接缝的替换成本是否已评估?
- 是否从最低成本接缝开始?
- 替换后是否验证了原始行为不变?
- 是否保留了回退分支?
内容种子
- 可衍生文章选题:《五种语言的接缝地图:从 C 到 Python 如何找到隐藏的测试点》
- 可设计课程模块:《接缝工作坊:在真实代码中定位和利用接缝》
- 可提出咨询问题:「你们团队的遗留代码库中,哪些模块的接缝最容易利用来建立第一道安全网?」
模型二:依赖破坏技术谱系(Dependency Breaking Techniques)
模型定义 将"破坏代码中阻碍测试的依赖"这一目标,拆解为一套分层技术谱系:从最轻量的运行时替换(传参、提取接口),到中等成本的编译时替换(包替换、文件作用域),再到最重量级的架构改造(引入服务定位器、框架隔离)。选择哪一层取决于依赖的类型、耦合深度和修改预算。
(图说明:根据耦合深度和预算约束选择最合适的依赖破坏技术层级。)
原书论证 Feathers 系统性地列举了约 30 种依赖破坏技术,但真正有骨架意义的是其分层逻辑:最轻量的是运行时替换(不改代码结构,只改运行方式),中等的是编译时替换(通过宏、包替换改变编译结果),最重的是架构级隔离(引入新架构层来隔离依赖)。书中反复强调的原则是**"用最小的侵入性达到可测试的目的"**——不要为了测试而大幅重构,而是找到刚好够用的那一层技术。
迁移场景
- 前端遗留项目改造:React 旧项目中 class 组件直接 import 全局样式和 API 调用。最轻量方案是提取 API 调用为独立模块(函数提取层),中等方案是用 React Context 包裹(服务定位器层),最重方案是引入微前端架构(框架隔离层)。
- 金融系统合规改造:核心交易引擎直接调用计息模块,而计息模块依赖央行利率数据库。在计息模块的数据库访问层创建接口并注入测试替身(提取接口层),可以在不连接央行数据库的情况下验证交易逻辑。
- 数据管道重构:ETL 脚本直接调用生产数据库。用环境变量控制连接字符串(参数传入层),切换到测试数据库运行验证。
失效边界
- 运行时反射依赖:当代码通过反射动态发现依赖时(如某些 DI 框架的运行时扫描),编译时替换无法拦截。
- 并发竞争条件:依赖破坏解决了功能隔离问题,但不能解决并发时序问题——测试通过不代表并发安全。
- 隐式依赖:当依赖通过日志系统、消息队列或数据库副作用传递时,简单的接口提取无法完整捕获。
改造方法
- 需要补一个变量:引入「破坏成本评估」步骤——在选择技术层级前,先估算每种技术的修改行数、影响范围、所需时间,做成本效益排序。
- 改造后形式:
识别依赖类型 → 列出候选破坏技术 → 估算每种的成本和效果 → 选择 ROI 最高的技术 → 执行 → 验证
*行动接口(3 套 SOP)
🟢 小白版 SOP
- 触发条件:一个函数需要测试,但它直接依赖了文件系统、网络或数据库。
- 执行步骤:
- 观察函数的参数列表——如果依赖是通过参数传入的,你已经有一个天然接缝。
- 如果依赖是函数内部 new 出来的,把 new 操作移到函数外部,通过参数传入(依赖传入法)。
- 如果连参数都没法改(因为其他调用者太多),提取一个接口,让依赖实现这个接口,然后通过工厂方法或 setter 注入。
- 验证标准:传入测试替身后,函数可以脱离真实依赖独立运行。
- 回滚机制:用 Feature Flag 控制新旧路径,出问题时切换回旧路径。
🟡 老手版 SOP
- 触发条件:面对一个依赖链条极深的模块,单点破坏不够用。
- 执行步骤:
- 绘制完整依赖链(A → B → C → D → 外部系统)。
- 在依赖链的薄弱环节(最容易破坏的点)切入——通常是最靠近测试目标的那一层。
- 用"依恋泥球"策略:先在一个小区域内建立完整测试覆盖,再逐步向外扩张。
- 对每个新扩张的区域,评估是否需要升级破坏技术的层级。
- 验证标准:每扩张一个区域,新增的测试全部通过,且原有测试不受影响。
- 常见进阶陷阱:在依赖链的中部插入破坏层,导致上游和下游都需要适配,成本爆炸。
🔵 团队版 SOP
- 触发条件:团队决定在某个核心模块上系统性地建立测试覆盖。
- 角色 × 步骤矩阵:
- 技术负责人:确定破坏技术的选择标准和优先级框架。
- 开发者 A/B/C:各自负责依赖链的一个分段,在约定的接缝处用统一的技术层级建立测试。
- 代码审查者:检查每个破坏点是否符合团队约定的侵入性上限。
- 验证标准:所有分段测试合并后,集成测试通过率 ≥ 98%。
- 回滚机制:每个分段独立可回滚,按分段粒度进行代码分支管理。
决策检查清单
- 依赖类型是否已分类(运行时/编译时/架构级)?
- 是否已评估每种破坏技术的修改成本?
- 是否选择了最低侵入性的可行方案?
- 破坏后的代码是否保持了可读性?
- 是否有回退路径?
内容种子
- 可衍生文章选题:《30 种依赖破坏技术的决策树:如何在 5 分钟内选出最适合的那一种》
- 可设计课程模块:《依赖破坏实战:从参数传入到服务定位器的渐进式改造》
- 可提出咨询问题:「你们团队修改代码时,最常遇到的阻碍测试的依赖类型是什么?目前用什么方式处理?」
模型三:特征测试安全网(Characterization Testing)
模型定义 在你理解代码的行为之前,先用测试忠实记录其当前行为(包括 Bug),将隐式的、不可预测的行为转化为显式的、可验证的契约。这些测试不判断对错,只锁住现状——它们是你修改代码时的"地基测量报告"。
(图说明:特征测试先忠实记录现状,再为修改提供保护——Bug 也是需要被保护的信息。)
原书论证 Feathers 区分了"特征测试"与"单元测试"的根本差异:单元测试描述的是代码应该怎么做,特征测试描述的是代码实际在怎么做。他用大量案例说明,当你开始修改遗留代码时,最大的恐惧不是"我改错了",而是"我不知道原来的行为是什么"。特征测试通过锁定现状消除了这种恐惧。即使代码里有 Bug,至少你不会在修改过程中意外破坏那个 Bug 的"副作用"——有些下游系统可能正依赖这个 Bug 的行为。
迁移场景
- 遗留 API 迁移:老 API 返回的数据格式中有一些不规范的字段(如某个日期字段有时返回字符串、有时返回时间戳)。在迁移前,用特征测试锁定每个端点的返回格式,包括那些不规范的部分,确保迁移后行为完全一致。
- 数据库 Schema 升级:老表结构中有些字段的 NULL 处理逻辑隐含在业务代码中。特征测试可以精确捕获每个场景下 NULL 值的处理方式,避免升级后的行为变化。
- 配置迁移:系统从 XML 配置迁移到 YAML 配置。先用特征测试锁定每种配置的运行时行为(超时值、重试次数等),确保迁移后行为不变。
失效边界
- 特征测试无法发现"应该做但没做"的行为:它只能捕获已有行为,无法发现遗漏的功能需求。
- 高并发/异步场景:特征测试在单线程下容易编写,但在并发场景下难以稳定复现特定行为序列。
- 时间/随机依赖:当代码行为依赖当前时间或随机数时,特征测试可能产生不确定结果。
改造方法
- 补充变量:引入"特征测试覆盖率"指标——不是代码行覆盖,而是行为路径覆盖。优先为高风险行为路径编写特征测试。
- 改造后形式:
梳理行为路径 → 按风险排序 → 逐条编写特征测试 → 锁定行为契约 → 再启动修改
*行动接口(3 套 SOP)
🟢 小白版 SOP
- 触发条件:需要修改一段你不完全理解的代码。
- 执行步骤:
- 找到这段代码的入口(被谁调用、接收什么输入、输出什么结果)。
- 为每种已知的输入场景写一个测试,验证当前的输出——不要判断对错,只记录事实。
- 运行测试,确认它们全部通过(如果某些失败了,说明你还没完全理解行为)。
- 现在开始修改代码,每次修改后运行所有特征测试。
- 验证标准:特征测试在修改前全部通过,修改后依然全部通过。
- 回滚机制:如果某个特征测试失败了,先分析是预期行为变化还是意外回归,意外回归则立即回退修改。
🟡 老手版 SOP
- 触发条件:面对一个行为极其复杂的模块,需要在保证多个下游系统不受影响的前提下进行修改。
- 执行步骤:
- 从依赖图入手,找到所有消费该模块输出的下游系统。
- 为每个下游系统的关键消费场景编写特征测试(从消费端而非提供端编写)。
- 将这些测试组织为"行为契约套件",按业务重要性分级(P0/P1/P2)。
- 修改过程中,P0 级测试必须始终通过,P1 级允许有条件失败(需确认影响),P2 级记录但暂不阻塞。
- 验证标准:P0 特征测试修改后 100% 通过,P1 通过率 ≥ 95%。
- 常见进阶陷阱:为代码的实现细节(而非行为)写特征测试——实现改变但行为不变时测试也会失败,导致"假阳性"阻碍修改。
🔵 团队版 SOP
- 触发条件:多团队协作修改共享核心模块。
- 角色 × 步骤矩阵:
- 核心模块团队:编写并维护 P0 级特征测试套件。
- 各消费方团队:编写各自消费场景的 P1/P2 级特征测试。
- CI/CD 负责人:将特征测试套件集成到流水线中,修改前自动运行完整特征测试。
- 验证标准:所有 P0 测试通过 + 各消费方的 P1 测试通过率 ≥ 95%。
- 回滚机制:CI 红灯后自动阻断合并,触发代码审查。
决策检查清单
- 是否已识别代码的所有外部行为(不只是主路径)?
- 特征测试是否忠实记录了当前行为(包括异常路径)?
- 测试是否覆盖了下游系统的消费场景?
- 修改前后特征测试结果是否一致?
- 是否区分了"预期行为变化"和"意外回归"?
内容种子
- 可衍生文章选题:《特征测试 vs 单元测试:为什么遗留代码需要一种完全不同的测试哲学》
- 可设计课程模块:《为未知行为画像:特征测试的工作坊实践》
- 可提出咨询问题:「你们在修改核心模块时,如何确保不会破坏下游系统的隐式依赖?」
模型四:修改风险分层模型(Change Risk Stratification)
模型定义 将遗留代码的修改区域按"依赖深度 × 行为复杂度 × 测试覆盖度"三个维度分层,识别出「高风险热区」(依赖深、行为复杂、无测试覆盖)和「低风险冷区」(依赖浅、行为简单、有测试覆盖),优先在冷区建立安全网,逐步向热区推进。
(图说明:先从冷区建立安全网,再逐步向热区推进,禁止在热区裸奔修改。)
原书论证 Feathers 在多处强调"不要直接修改你最害怕的代码"——这听起来像常识,但在实际项目中经常被违反。他提出的策略是先外围、后核心:从最容易建立测试的代码入手,逐步建立安全网,最后才触及核心热区。这个模型的底层逻辑是风险对冲——每在冷区多建立一份测试覆盖,热区修改时的安全边际就增加一分。
迁移场景
- 微服务拆分:先从边界清晰的工具类服务(冷区)开始建立测试和独立部署能力,再处理核心交易引擎(热区)。
- 技术栈迁移:先迁移配置管理和日志系统(冷区),积累迁移经验和安全机制后,再迁移核心业务逻辑(热区)。
- 团队知识积累:新成员先从冷区代码入手建立信心和系统理解,再逐步深入热区——避免一上来就碰核心逻辑导致的信心崩塌。
失效边界
- 冷区可能不存在:如果整个代码库都没有测试且依赖全面纠缠,冷区可能是空的——此时需要先"人造冷区"(通过提取函数/模块将一小块代码孤立出来)。
- 热区修改的紧迫性:有时热区的 Bug 影响极为严重,等不到安全网建好就必须修——此时需要临时的"外科手术式"修改策略。
改造方法
- 补充变量:加入「修改紧迫度」维度,与风险分层结合,形成三维决策矩阵。
- 改造后形式:
风险评估 → 紧迫度评估 → 生成修改优先级排序 → 按序推进
*行动接口(3 套 SOP)
🟢 小白版 SOP
- 触发条件:面对一个大代码库,不知道从哪里开始改。
- 执行步骤:
- 找出代码库中最简单的、几乎没有外部依赖的函数或类。
- 为这些简单代码写测试——这是你的冷区起步点。
- 每完成一个冷区的测试覆盖,向外扩展一步,处理稍微复杂一点的代码。
- 验证标准:每完成一个区域的测试,该区域的修改不再需要恐惧。
- 回滚机制:不急于扩展,如果新区域的测试编写遇到重大阻碍,退回上一个稳定的冷区。
🟡 老手版 SOP
- 触发条件:需要制定整个模块的系统性改造计划。
- 执行步骤:
- 绘制模块的类依赖图,标注每个类的对外依赖数和被依赖数。
- 计算每个类的风险分值(被依赖数 × 外部依赖数 / 现有测试数),生成热力图。
- 将热力图转化为修改路线图:先冷区建测试,再温区做小重构,最后热区做大改造。
- 验证标准:路线图中每个阶段的进入条件和退出条件都明确可验证。
- 常见进阶陷阱:热力图过于精确导致"分析瘫痪"——用粗粒度的三级分类(热/温/冷)就足够启动行动。
🔵 团队版 SOP
- 触发条件:团队需要在持续交付的同时治理技术债务。
- 角色 × 步骤矩阵:
- 技术负责人:维护风险热力图,每季度更新一次。
- 开发者:每次修改代码时,先查热力图确认自己在什么区域,选择对应的风险策略。
- 产品经理:理解热力图逻辑,在排期时为热区修改预留额外缓冲。
- 验证标准:团队连续 3 个月无热区裸奔修改导致的线上事故。
- 回滚机制:任何热区修改必须经过额外的代码审查和技术负责人签字。
决策检查清单
- 是否已绘制模块的风险热力图?
- 当前修改是在热区、温区还是冷区?
- 是否遵循了"先冷后热"的推进顺序?
- 热区修改是否有额外的安全保障?
- 路线图的阶段入口和退出条件是否明确?
内容种子
- 可衍生文章选题:《遗留代码的热力图:如何用一张图决定团队未来三个月的重构顺序》
- 可设计课程模块:《风险分层实战:为你的代码库画出第一张热力图》
- 可提出咨询问题:「你们团队目前的代码修改中,有多少比例是在热区裸奔?风险是否被充分认知?」
模型五:角色接口隔离(角色对象模式 / Role Interface)
模型定义 当一个类承担了过多角色(被多个不同场景以不同方式使用)时,按使用场景将其拆分为多个"角色接口"——每个接口只暴露某个使用场景真正需要的行为。这使得测试可以针对单一角色注入精确的测试替身,而不是模拟一个庞大而模糊的"全能对象"。
(图说明:按使用场景拆分角色接口,让测试替身变得轻量且精确。)
原书论证
Feathers 指出,遗留代码中很多类之所以难以测试,不是因为它太复杂,而是因为它承担了太多角色。一个 OrderProcessor 类同时处理订单验证、库存查询、支付接口调用和通知发送——测试任何单一功能都需要模拟所有其他功能的依赖。角色对象模式的核心洞察是:接口应该按使用者的需求来定义,而不是按被使用者的实现来定义。这与 SOLID 原则中的接口隔离原则(ISP)呼应,但在遗留代码改造场景中更具操作性。
迁移场景
- 单体应用拆分:一个
UserService同时提供注册、认证、权限查询和审计日志功能。按角色拆分为RegistrationService、AuthenticationService、AuthorizationService、AuditService,每个可以独立测试。 - 前端组件重构:一个大型 React 组件同时处理数据获取、UI 渲染、用户交互和分析埋点。按角色拆分后,数据获取逻辑可以用 mock 数据独立测试,UI 渲染可以用快照测试独立验证。
- 数据库访问层改造:一个
DataAccess类同时处理 CRUD、缓存、连接池管理和查询优化。按角色隔离后,业务代码只依赖 CRUD 接口,缓存和连接池管理变成可独立测试的横切关注点。
失效边界
- 角色边界模糊时:当多个使用场景的行为高度重叠时,强行拆分角色接口可能导致接口碎片化,维护成本反而上升。
- 性能敏感场景:角色接口的间接调用可能引入微小的性能开销,在高频调用路径上需要权衡。
- 向后兼容约束:如果角色拆分后旧的调用者需要适配新接口,在大型系统中适配成本可能很高。
改造方法
- 需要补的变量:引入「角色使用频率」和「角色间重叠度」两个指标。使用频率高且重叠度低的角色优先拆分;重叠度高的角色考虑保持为一个接口但按方法分组。
- 改造后形式:
统计使用场景 → 识别角色边界 → 评估重叠度 → 按优先级拆分 → 渐进迁移调用者
*行动接口(3 套 SOP)
🟢 小白版 SOP
- 触发条件:测试一个类时,发现需要模拟 3 个以上的依赖。
- 执行步骤:
- 列出这个类的所有公有方法。
- 按"谁在用这些方法"分组——每组就是一个候选角色。
- 为每组提取一个接口,原类实现所有接口。
- 测试时只注入当前场景需要的角色接口。
- 验证标准:每个角色接口的测试替身不超过 3 个方法。
- 回滚机制:保留原始接口作为兼容层,调用者可以渐进迁移。
🟡 老手版 SOP
- 触发条件:核心领域对象承担了过多职责,导致所有测试都很臃肿。
- 执行步骤:
- 运行依赖分析工具,找出该对象的所有调用者及其调用的方法子集。
- 用聚类算法或手动分析将方法调用模式聚类——每类就是一个自然的角色。
- 设计角色接口,确保每个接口的方法集是内聚的(高内聚低耦合)。
- 逐步将调用者迁移到对应的角色接口,每迁移一批就运行全量测试确认无回归。
- 验证标准:所有调用者完成迁移,原始类只保留角色接口的组合实现。
- 常见进阶陷阱:角色拆分过度导致"接口爆炸"——5 个以上的角色接口通常意味着拆分粒度过细。
🔵 团队版 SOP
- 触发条件:团队决定对领域模型层进行系统性重构。
- 角色 × 步骤矩阵:
- 领域专家 + 架构师:定义领域概念中的自然角色边界。
- 开发者:按角色边界提取接口并实现。
- 测试负责人:为每个角色编写独立的测试套件。
- 验证标准:每个角色接口的测试套件独立运行通过率 100%,集成后通过率 ≥ 98%。
- 回滚机制:按调用者批次回滚,不按角色回滚。
决策检查清单
- 是否已完整统计所有使用场景?
- 角色边界是否按使用场景而非实现方式定义?
- 每个角色接口是否保持高内聚?
- 是否评估了角色拆分的性能影响?
- 调用者迁移是否有渐进路径?
内容种子
- 可衍生文章选题:《为什么你的测试总是需要 Mock 一万个依赖——角色接口是解药》
- 可设计课程模块:《从胖对象到角色接口:领域模型的减负手术》
- 可提出咨询问题:「你们团队在写测试时,最痛苦的 Mock 场景是什么?背后是否是角色边界不清的问题?」
CH.05🧠 费曼检验
情境问题
张伟是一个后端开发者,刚接手一个运行了 6 年的电商订单系统。系统没有自动化测试,每次修改都靠手动回归,上周一个"小修改"导致了 3 小时的线上故障。现在产品经理要求在订单流程中增加一个"分拆订单"功能,涉及订单创建、库存扣减、支付发起三个核心环节。老板给了两周时间。张伟打开代码发现:OrderService 类有 2000 行,直接调用了 InventoryClient、PaymentGateway、NotificationService,以及一个连接生产数据库的 DbConnection。没有任何接口,所有依赖都是直接 new 出来的。
请用本书的模型分析张应该如何推进。
参考解法框架
先用修改风险分层模型将三个环节(订单创建、库存扣减、支付发起)的风险等级排序——订单创建逻辑最复杂且被依赖最多(热区),库存扣减有外部 API 依赖(温区),支付发起有明确的输入输出接口(可作为冷区起步)。再用接缝模型识别每个环节中最容易利用的接缝——支付发起可能有明确的 HTTP 调用可以被 mock(函数接缝),库存扣减可以通过提取接口破坏依赖(角色接口),订单创建需要先理清依赖图再决定破坏策略。用特征测试安全网锁定三个环节的当前行为,再开始实现新功能。
好的回答应包含的要素:明确的风险分层判断、具体的接缝识别过程、特征测试的编写策略、依赖破坏技术的选择理由、对两周时间约束的优先级取舍。
5 个常见误解
误解:"遗留代码就是老代码。" 澄清:Feathers 对遗留代码的定义极其精确——没有测试的代码就是遗留代码,无论它多新。一段昨天写的、没有测试覆盖的代码,在修改时就是遗留代码。
误解:"修改遗留代码前应该先全面重构。" 澄清:全面重构是最大的陷阱之一。本书的核心主张恰恰相反——不要重构,而是先建立安全网(测试),在安全网保护下做小规模、增量式的修改。
误解:"依赖破坏技术是为了让代码变好。" 澄清:依赖破坏的唯一目的是让代码变得可测试。它可能让代码结构暂时变得更复杂(多了接口、多了参数),这是必要的代价,不是重构美化。
误解:"特征测试应该覆盖所有代码路径才算合格。" 澄清:特征测试的目标不是覆盖率,而是锁定你即将修改的那部分代码的行为。100% 覆盖率是理想状态,但先覆盖修改影响范围内的路径就够了。
误解:"这本书教的是如何一次性解决技术债务。" 澄清:这本书教的是在技术债务存在的情况下安全工作的方法。它不承诺消灭债务,而是让你在债务的压力下依然能安全地交付价值。
12 岁孩子版
第一本书在说一件什么事?你拿到了一台没人教你怎么修的机器,但你得换个零件。 以前大家以为该怎么做?要么不修,要么硬拆,要么买台新的。 作者发现其实是这样的?机器上有好几个地方可以偷偷拆开看里面的结构,你先从最容易打开的地方下手,把看到的东西画成图纸。 所以你可以这么用?先画好图纸再动手换零件,换的时候随时对照图纸确认没弄坏别的东西。 但要注意?图纸只能画你看到的部分,有些隐藏的管道你可能发现不了,换零件时还是得小心。
CH.06📝 全书评估
真正解决了什么问题? 解决了软件开发者最普遍但最缺乏系统方法论的问题——"如何安全地修改没有测试保护的代码"。这不是理论问题,而是每天都在发生的真实困境。
核心模型原创性如何? 接缝概念和依赖破坏技术谱系有较高的原创性,尤其是将约 30 种技术按侵入性分层并给出选择指南,这在同类书中是独一无二的。特征测试概念本身不算新(与"回归测试"一脉相承),但在遗留代码语境下的重新定义很有价值。
证据质量如何? 以作者在 C++ 和 Java 遗留代码项目中的实战经验为主,案例具体且可复现。但缺少大规模量化数据(如采用这些技术后 Bug 率下降了多少),更多是方法论层面的论证。
最大盲区是什么? ①对分布式系统和微服务架构的覆盖不足(书成于 2004 年,当时微服务尚未流行);②对并发/异步代码的修改策略讨论较少;③几乎没有涉及团队协作和项目管理层面的组织问题(如如何说服管理层投入时间建测试安全网)。
书籍坐标:在软件工程类书籍中,本书处于"代码维护与演化"这一细分领域的奠基位置。上游是《重构》(Martin Fowler,提供重构模式但不讨论测试安全网),下游是《可持续重构》和《持续交付》(Jez Humble,在本书基础上扩展到交付流程层面)。
CH.07🔗 跨书关联
与《重构:改善既有代码的设计》的关联
- 共振点:两本书都关注如何安全地修改已有代码。《重构》提供了 72 种具体的重构手法(如提取方法、搬移字段),本书提供了让这些手法得以安全执行的前提条件——测试安全网。
- 冲突点:《重构》某种程度上假设了测试的存在或重构者的高超技巧;本书则明确指出,在没有测试的遗留代码中直接使用《重构》的手法是危险的。本书会说:先用接缝和依赖破坏建立测试,再用《重构》的手法优化结构。
- 为什么接着读:读完本书再读《重构》,你将拥有完整的"修改代码工具箱"——本书教你如何安全地到达可以重构的状态,《重构》教你到达之后如何进一步优化。
与《测试驱动开发》的关联
- 共振点:Kent Beck 的 TDD 与 Feathers 的方法形成了互补的两面——TDD 是"从零开始时怎么写好代码",本书是"代码已经写坏了时怎么修"。两者共享同一个核心信念:测试是安全修改的基石。
- 冲突点:TDD 要求"先写测试再写代码",但在遗留代码中,你经常面对的是"代码存在但测试不存在"的倒序情况。本书的特征测试正是解决这个"倒序困境"的方法。
- 为什么接着读:读完本书理解了"如何为已有代码补测试"之后,再读 TDD 会在新项目中从源头避免遗留代码问题,形成完整的测试策略闭环。
与《持续交付》的关联
- 共振点:《持续交付》强调通过自动化流水线和测试保障软件可以随时安全部署。本书的接缝和依赖破坏技术正是让遗留代码能够接入持续交付流水线的关键手段。
- 冲突点:《持续交付》偏向流程和基础设施层面,本书偏向代码层面。两者在"测试"上的交集很多,但粒度不同——一个关注部署管道中的测试策略,一个关注代码内部的可测试性改造。
- 为什么接着读:读完本书掌握代码层面的可测试性改造后,再读《持续交付》能理解如何将这些改造嵌入完整的交付流程,实现从"代码安全"到"部署安全"的跨越。
知识网络位置
本书在这条主题脉络里的位置:
- 上游(先读):《重构:改善既有代码的设计》——理解重构的基本模式和手法
- **下游(再读):《持续交付》——理解代码改造如何嵌入交付流程
- 对照读:《敏捷软件开发:原则、模式与实践》(Robert C. Martin)——从 SOLID 原则的角度理解依赖管理,与本书的实践视角形成对照
CH.08✨ 深度洞察摘录
遗留代码的本质不是"旧"而是"无测试"
- 来源:《修改代码的艺术》核心定义章节
- 类型:认知颠覆
- 核心内容:Feathers 对"遗留代码"的定义颠覆了行业共识——不是运行了多年的代码才算遗留,而是任何没有测试覆盖的代码在修改时都是遗留代码。一段昨天写的、没有测试的代码,今天要修改时你面对的困境与修改十年前的代码完全相同。这个重新定义将"遗留代码问题"从少数老旧系统的专属困境,变成了所有开发者的日常挑战。
- 可迁移到:团队技术债务评估——不要按代码的年龄来排优先级,而是按测试覆盖度来排;新写的无测试代码应与旧代码一视同仁地标记为"待治理"。
接缝是代码的"可替换关节"
- 来源:《修改代码的艺术》接缝模型章节
- 类型:可迁移模型
- 核心内容:代码中的接缝就像人体的关节——骨骼(核心逻辑)本身是坚硬不可弯折的,但关节(接缝)允许你在不折断骨骼的前提下改变运动方向。找到接缝就是找到不改变核心逻辑就能改变程序行为的支点。这个模型揭示了一个反直觉的事实:改造遗留代码的关键不在于理解代码做了什么,而在于找到代码中行为可以被接管的点。
- 可迁移到:系统架构设计——在设计新系统时主动创建更多接缝(如依赖注入、策略模式、事件驱动),为未来的可测试性和可维护性留出空间。
最小侵入性原则:为了测试不要引入更大的混乱
- 来源:《修改代码的艺术》依赖破坏章节
- 类型:金句级表达
- 核心内容:为了建立测试而引入的代码改造,其侵入性必须严格低于它所保护的修改本身——如果你为了测试一个函数而重写了整个模块,那你已经制造了比原始问题更大的风险。这个原则是很多团队在"治理技术债务"时失败的根因:他们把"治理"变成了"重写"。
- 可迁移到:所有技术改造决策——在选择改造方案时,方案本身的风险必须低于它所解决的问题的风险。这是一条通用的工程约束。
先测量再手术:不知道现状就不要动刀
- 来源:《修改代码的艺术》特征测试章节
- 类型:跨书共振
- 核心内容:特征测试的哲学与医学中的"诊断先于治疗"完全一致——你不会在没做检查的情况下给病人动手术。但在软件开发中,无数开发者正在对没有"检查报告"(测试)的代码"动刀"(修改)。特征测试就是那份检查报告,它不判断对错,只告诉你"现状是什么"。这份看似无用的信息,恰恰是你修改代码时最重要的安全边界。
- 可迁移到:任何涉及系统变更的场景——数据库迁移前的现状审计、组织变革前的基线评估、政策调整前的现状摸底,本质上都是"特征测试"思维的延伸。