接手一个旧项目,最容易看到的是结构问题:几千行的类、全局状态、看不懂的条件分支。换一套清楚的设计,似乎比在原地修补更省事。
但旧代码还保存着另一类东西:没有进入文档的兼容行为、过去事故留下的限制,以及调用方已经习惯的结果。重写计划如果只统计功能点,很容易漏掉这些承诺。
Michael Feathers 的《修改代码的艺术》关注如何在缺少测试保护的代码里安全修改。书中整理了把代码放进测试环境、识别修改位置和解除依赖的具体方法。出版方介绍
接手时的第一项任务,可以很小:找到本次需要修改的行为,并建立能及时看见意外变化的反馈。
先找到一个需要回答的问题
以下是一个构造的移动端工程场景。
一个旧附件模块负责下载、生成文件名和打开预览。现在需要修复重名文件覆盖的问题。下载类直接读取当前账号,调用全局网络对象,并在完成后打开页面。测试一运行,就会访问网络或者要求界面环境。
通读整个附件系统当然能增加了解,但本次真正需要回答的是:两个同名附件在什么条件下覆盖?目录是否按账号隔离?文件名生成失败时,下载是否还会继续?
沿这几个问题寻找输入和输出,比先画完整架构更容易获得有效信息。可以从下载入口追到写文件的位置,记录实际路径、完成回调和错误结果;其他暂时无关的逻辑保留原状。
旧代码中的奇怪分支也先留下。有些确实没有用途,有些可能是在处理服务器返回的空标识。不能仅凭“不符合新设计”就判断它可以删除。
特征测试记录实际发生的行为
Feathers 在一次公开访谈中解释,特征测试用于理解系统现有行为。它与按照需求判断对错的测试,关注点有所不同。作者访谈,2021
在附件示例里,先固定输入,观察实际生成的目录和文件名,再把这些结果写进测试。普通文件名、无扩展名、空标识和相同名称,都可以成为样例。测试最初回答的是“修改前发生了什么”。
假如旧代码把所有空标识附件写进同一个目录,记录这个行为并不表示认可它。测试可以注明它是现状;与需求方确认之后,再把预期改为新的规则,作为本次有意变更。
这样能区分两种失败:一个是修复覆盖问题时,意外改变了正常附件的路径;另一个是旧行为已被明确替换,相应测试应该更新。没有这层区别,失败的测试很容易被批量改成新结果,失去保护作用。
也不要把运行过程中的一切都固定下来。临时目录前缀、当前时间和随机标识会让测试不稳定。应控制这些输入,或只比较真正相关的结果。但如果问题恰好来自时间或标识碰撞,就必须保留相应差异,不能通过归一化把错误擦掉。
找接缝,让外部依赖可以替换
原方法难以测试,往往因为一次业务计算必须同时启动很多外部设施。在附件模块中,要验证文件名规则,却先要登录、联网和展示页面。
接缝提供了一种看法:在已有连接处替换协作者,让被测逻辑仍然按原来的方式运行。Feathers 在访谈中用接口和构造参数说明这类替换点,也讨论了直接嵌入全局资源调用时为何难以做到。接缝的解释
示例项目如果已经允许传入下载器,就可以用测试替身返回固定数据,不再访问真实网络。若完全写死在内部,则可以先增加一个最小的依赖入口,生产环境仍使用原下载器;这本身是代码修改,需要已有入口验证和仔细评审保护。
接缝不限于面向对象接口。不过,没必要为本次附件修复引入一套通用依赖注入框架。需要控制网络,就先隔离网络;需要控制账号,就固定本次运行采用的身份。边界应由测试障碍决定。
测试替身也应保留相关语义。真实下载器可能在异步队列回调、可能失败、可能在取消后返回结果。如果替身总是同步成功,它只证明了最容易的路径,无法帮助验证账号切换时的旧任务。
修改范围小,反馈才容易解释
一种可行顺序是先捕获正常下载的结果,再隔离文件名生成依赖,随后补上同名碰撞场景,最后修复规则。每一步只解决一个问题,测试失败时就能缩小原因。
不需要先把整个项目覆盖起来。对本次变化,优先保护受影响的入口和重要消费者。文件名生成正确,还要检查预览是否使用了同一路径,否则局部测试通过,用户仍打不开附件。
验证范围可以分层:纯计算测试覆盖命名边界,临时目录里的集成测试检查写入结果,少量完整流程确认下载与预览连接正常。不同层解决不同风险,无需把每个边界都放到昂贵的界面自动化中。
如果第一轮特征测试就不稳定,应先找出不可控输入。真实网络、全局账号或残留文件夹造成的差异,不适合用多次重跑掩盖。测试只有在同样条件下给出一致反馈,才能用于判断修改是否安全。
重写也需要先识别旧行为
渐进修改不是所有系统的唯一答案。底层平台停止支持、原数据模型无法承载业务,或者隔离成本过高,都可能使替换方案更合理。
但替换仍然需要回答相同问题:哪些行为必须保留,哪些允许改变,旧数据怎样进入新系统,以及失败之后如何恢复。能够表达这些边界的测试与样例,会成为新旧实现之间的比较依据。
附件系统如果决定更换存储方案,可以先让新实现读取旧路径,或者安排经过验证的数据迁移。具体选择要看数据规模和使用方式;“重新写一遍下载器”本身并没有解决用户已有文件的位置问题。
当一个缺陷已经明确,特别是涉及权限或数据丢失时,也不能为了保持旧行为无限拖延修复。需要明确改变的目标,建立能暴露缺陷的样例,再验证修复及其兼容影响。特征测试提供事实,是否保留由业务与风险决定。
给接手后的第一天一个小目标
我更倾向于先完成一张简短记录:本次要改什么,入口在哪里,依赖哪些外部状态,已观察到哪些行为,还有什么不知道。
随后尝试让一个相关场景在受控环境里重复运行。若做不到,先解决最妨碍反馈的那项依赖。等到修改前后的差异能够解释,再扩大范围。
这一步没有新架构那么显眼,却能直接改变工作方式。下一次面对旧模块,开发者可以先运行样例,再判断修改;不用把每个历史分支都理解透,也不必把第一次发现意外的机会留到上线之后。