先查清哪些行为还有人用,再用垂直切片和并行变更迁移;退役时引用与使用一起看。
遗留系统的需求常常藏在用户操作、批处理、报表和接口调用里,只读一份正式文档,未必知道系统真正承担了哪些工作。我的笔记把“功能对等”和“功能价值”写在同一页,就是提醒自己:迁移前先确认哪些行为还值得保留。
功能对等不等于值得保留
要调查的范围不只是页面,还有:
- 批处理流程
- 外部集成
- 核心算法
- 报表与数据
还得做点“考古”:有些行为为什么存在,要回到旧实现或使用记录里找原因。
判断保留什么,要把两个问题分开问。功能对等问的是新旧行为是否一致,功能价值问的是这条行为还有没有人在用。一条行为可以完整地搬到新系统,但已经没人需要它了,一致本身就不构成保留的理由。
Thoughtworks 那组 Patterns of Legacy Displacement 文章给这件事起了名字,叫 Feature Parity,吃过亏的人干脆叫它“feature parity trap”。默认答案“照旧系统做一遍”听起来最省事,代价是理解现状的成本被系统性低估:钱花了不少,业务没改善,有时连有用的东西也一起丢了。作者的态度很明确:“In general this is a pattern that we don’t recommend.”
调查可以从一个用户怎样完成工作开始。价值流图帮着描述过程,业务能力划分帮着找边界,事件风暴帮着讨论状态变化。用哪几种,看缺的是哪部分信息。
这几样在那组文章里正好归在“Understanding the problem”一类,再加一个 Create Town Plan。价值流图的定义就是“描述用户如何完成工作的产物”。EventStorming 官网说它可以挖一整条业务线的流程,顺带暴露不同角色之间的矛盾。要注意的是,这些模式页大多还是没写完的 stub,拿来当思路可以,别当权威定义引。
决定保留的行为,再建对应的验证。用户故事和端到端场景覆盖可见流程,接口还要顾及提供方和消费者两端。只证明新系统内部运行正常,漏掉的可能恰好是旧系统的外部使用者。
用垂直切片交付一个看得见的变化
可以按业务拆分端到端流程。一个切片把某个用户行为涉及的界面、逻辑、数据和接口一起交付,实际结果看得见。切片大小仍要服从系统约束,别为了分阶段切断本来不可分割的行为。
为什么非切不可,Fowler 在 Strangler Fig Application 里说得很实在:替换一个正经的 IT 系统要很久,用户等不了新功能。整体重写意味着很长一段时间什么都交付不了。
高度优化的算法、受明确规范约束的计算,迁移时可能需要更严格的结果对照,工程和科学建模里的一些计算就可能属于这类。它们有现成的参照:旧实现算出的结果,或规范里写明的预期值。迁移时要逐项比对,流程跑通说明不了计算对不对。
Feature Parity 那篇恰好把这类计算列成“照抄合理”的例外:受固定外部规范约束、“identical input must give identical output”的科学或工程建模计算。它还给了一个可以照搬的做法:财务计算服务只换技术、不动规范时,旧实现周围那套测试就是现成的可执行规范。另一条路是分支抽象的变体:先加一层抽象让新旧实现共存,用 feature flag 让两套同时跑,比对返回结果,整个过程系统始终能正常构建和运行。
同一页也提醒,这类场景通常是局部的,不该拿它来决定整个改造的范围。
接口变化分三步走
Parallel change 把接口变更分成扩展、迁移、收缩三步。下面按手写箭头重画。
查看 Mermaid 源码
flowchart LR expand["扩展:增加新接口,保留旧接口"] --> migrate["迁移:逐步更新调用方"] migrate --> contract["收缩:确认迁移完成后移除旧接口"]新旧接口共存期间,调用方有时间迁移。到了收缩阶段,必须知道还有没有旧调用方,否则“删除旧代码”的问题可能部署后才暴露。这个顺序也能用来理解接口和数据库演进,具体兼容策略还是取决于实际契约。
Evolutionary Database Design 给了一个数据库版本的例子:把表 customer 改名 client,同时留一个叫 customer 的兼容视图。这段新旧访问方式并存的过渡期可能是几个月,甚至几年。加一个可空列属于向后兼容的改动,可以先上库、后上代码。
收缩这一步最容易被拖掉,而 Parallel change 原页说得不客气:
If the contract phase is not executed you might end up in a worse state than you started.
暗启动、绞杀式迁移、反腐层和流量分配也是可选手段,各自解决不同的问题:有的帮着观察新实现,有的隔开新旧模型,有的逐步转移入口。选哪种,看眼前遇到的是哪一类障碍。
几种手段的代价和适用条件差得挺多:
| 手段 | 做什么 | 代价或限制 |
|---|---|---|
| 暗启动 | 调用新后端但不用它的结果,评估性能影响 | 通常要改现有系统;用户看不到差别,旧体验可能原样保留 |
| 绞杀式迁移 | 前面放 façade,逐步把请求切到新系统,旧系统无依赖后下线 | 拦不住请求、拿不到旧系统源码、系统很小或必须马上下线时不适用 |
| 反腐层 | 在语义不同的子系统之间翻译,挡住旧模型的污染 | 调用多一层延迟,多一个要运维的服务;不要往里塞业务规则 |
| canary | 只对随机抽出的一小部分用户打开新功能,对比两组业务指标 | 开关要主动清理 |
我觉得暗启动这一行最值得记:它观察的是负载下的表现,返回结果不参与业务判断,别指望它帮你验证逻辑对不对。“绞杀”这个词来自 Fowler 2001 年在昆士兰雨林看到的绞杀榕,最早叫 Strangler Application,后来改成 Strangler Fig Application,去掉暴力联想。
退役要同时看引用和实际使用
清理资产时有两种证据:静态依赖图告诉你谁引用谁,运行记录补上真实使用情况。两种要合起来看。只看搜索结果为空,可能漏掉动态调用;只看一段时间没有访问,也证明不了季节性流程不会再用到它。
搜索为空不可靠,有一个很常见的机制:Spring 的类路径扫描会自动发现带注解的类并注册成 bean,代码里可以完全没有对它的静态引用;扫到谁还取决于 includeFilters/excludeFilters 的配置。共享数据库是另一个坑,Evolutionary Database Design 专门提到,库上挂着“many applications and reports”时最难改,报表和批处理正是季节性调用方的老家。
使用证据怎么拿,Legacy Displacement 的建议是用埋点和现有日志看实际使用,别靠猜;还要往系统边界外看,影子 IT 里的 Access 数据库、版本化表格,常常暴露出真正还在跑的业务。
退役前,范围和实际使用可以按同一条顺序检查。
查看 Mermaid 源码
flowchart TD scope["确认退役范围与相关业务"] --> static["检查静态依赖"] scope --> dynamic["检查运行记录与外部使用者"] static --> assess["合并证据并确认迁移安排"] dynamic --> assess assess --> move["迁移调用方并验证行为"] move --> retire["按确认范围移除旧资产"]组织上也要跟上:持续改造需要稳定的责任和时间安排。一次性项目结束后,要是没人负责后续迁移,扩展阶段留下的兼容层就可能长期存在。
那组文章的总论下了一个好记的比例:“technology is at most only 50% of the legacy problem”,剩下的在组织结构、工作方式和领导层,不改这些,就该预期和上次一样的结果。Fowler 的说法更直接:没有组织文化和领导层的改变,新系统会变成同样一团乱。
兼容层长期存在为什么有害,Transitional Architecture 一页讲了:用不到的组件哪怕没人调用,也会拖累后来的团队维护和演进。Feature Toggles 那篇把开关形容成“有携带成本的库存”,是同一个道理。所以我会把收缩写进计划,而不是留给“以后”:建脚手架的时候就想好怎么拆,谁来拆。
留下几句话 读者来信
读到这里,有什么想法?欢迎留下你的看法。