彻底重写之所以诱人,是因为它承诺干净利落地告别过去,而这也正是它的危险之处。旧系统里藏着多年来没人写下的决定;你重建的同时它还在变;业务要等到最后才有收获,而那时才会发现遗漏的需求。我们默认不建议一次性替换,要这么做,得先拿出充分的理由。
先弄清系统实际做了什么
遗留系统的文档记录的是当年某人的意图。运行中的代码才是规格说明,缺陷也算在内,其中一些缺陷已经成了客户或下游报表依赖的“功能”。
Michael Feathers在《修改代码的艺术》中提出特征测试(characterization test):记录代码现在实际做什么,而不是它应该做什么。用真实的输入调用代码,看输出什么,再断言这个结果。之后测试失败,就说明行为变了,你可以判断这是不是有意的。
- 从能接触到的最高层入手,比如HTTP接口、批处理任务的输入输出文件或生成的报表。
- 尽量使用脱敏后的真实样本,古怪的情况都藏在真实数据里。
- 发现缺陷时不要在同一次改动里修复,先记录,再单独决定。
- 针对即将修改的代码写测试,不必追求整体覆盖率。
用绞杀者模式绕过去
绞杀者模式(strangler fig,由Martin Fowler命名)是逐步替换系统的做法:新功能在旧系统周围生长,一块一块接管职责,直到旧系统可以关闭。
关键是要有一个能改变请求走向的接缝:反向代理、API网关、消息路由或代码里的门面层。有了它,迁移就变成一连串可逆的小决定。
- 在旧系统前面加路由层,所有流量仍走旧代码,单独上线这一步。
- 选一个切片。合适的候选经常变化,或者卡住了业务想做的事,而且边界清晰。
- 构建替代实现并做对比。把真实流量复制一份发给它,与旧系统的响应比较,但不把它的结果返回给用户。
- 切一小部分流量,观察后再逐步增加。在有把握之前,保留回到旧代码的路径。
- 删除旧代码路径。这一步经常被跳过,跳过的结果就是同时运行两套系统。
第一个切片要真实,但出了问题也扛得住。只读的报表或查询往往是不错的起点,可以先验证路由、监控和部署,再去碰写数据的部分。
数据才是难点
这类项目很少卡在迁移代码上,卡住的通常是共享数据库。新旧系统同时写同一批表,旧表结构就会束缚新设计,两边还可能破坏彼此的假设。
任何时刻,每份数据只有一个权威系统,交接要明确。变更数据捕获(CDC)可以在旧系统继续写入时,为新系统维护一份只读副本。尽量避免应用层双写,一边成功一边失败时,你就有了对账问题。无论选哪条路,都要定期对账,比较两边数据并报告差异。
什么时候可以重写
有时重写确实是对的:系统小到几周就能替换,或者平台即将停止支持,又没有渐进改造的路径。即便如此,也要保留特征测试并安排并行运行,这样切换前才能证明新系统行为符合预期。
事先定好停止条件
渐进式工作容易漫无边际。提前约定做到什么程度算完成,让人头疼的部分解决后就要舍得收手。稳定且很少改动的模块可以留在原地,只要它仍有支持且安全。目标是得到一个能安全修改的系统,没人需要碰的旧代码并不妨碍这一点。
本周可以做的事
- 找出修改热点。运行
git log --since="1 year ago" --name-only --format="" | sort | uniq -c | sort -rn | head -20,看哪些文件改得最多。 - 问问业务方,系统让哪些想做的事变得困难,再看看与哪些热点重合。
- 围绕首要候选写五个特征测试。
- 画一下路由接缝可以放在哪。找不到的话,创建接缝就是第一项工作。