28.5 遗留系统集成 (绞杀者 / 防腐层)

一句话

集成遗留系统只有两条正当路径:防腐层(ACL) 把它的模型挡在边界外,绞杀者(Strangler) 一点点把它替换掉;两条都不做,新系统会慢慢长成旧系统的形状,并且再也没人敢动旧系统。

两条路径的分工

路径 做什么 何时用 代价
防腐层(Anti-Corruption Layer) 在边界做模型转换,旧系统的概念不外泄 旧系统长期存在、不能替换 适配器要维护;转换逻辑会变厚
绞杀者(Strangler Fig) 新实现逐步接管流量,旧系统萎缩直至下线 旧系统可被替代,且有明确边界 双跑期成本 + 路由切换
大爆炸重写 一次性替换 几乎从不推荐 风险与工期不可控,历史上成功率低

两者可以叠加:先用 ACL 隔离,再用绞杀者按功能切片迁移。

代码:ACL 的形态

// 旧系统:字段名诡异、语义含糊、错误码是数字
type legacyCustomer struct {
    CUST_NO  string `json:"cust_no"`
    STAT     int    `json:"stat"`      // 0=正常 1=冻结 9=注销(无文档)
    AMT_UNIT int    `json:"amt_unit"`  // 1=元 2=分(历史遗留)
}

// 防腐层:把旧模型翻译成领域模型,旧概念不外泄
func (a *legacyAdapter) GetCustomer(ctx context.Context, id string) (Customer, error) {
    raw, err := a.client.Fetch(ctx, id)
    if err != nil { return Customer{}, translateErr(err) }   // 错误码也翻译
    if raw.AMT_UNIT == 2 { /* 分 → 元 */ }
    return Customer{ID: raw.CUST_NO, Frozen: raw.STAT == 1}, nil
}

实测:ACL 到底挡住了什么(静态扫描)

把 “领域层代码” 当字符串做旧字段泄漏扫描:

写法 命中的旧字段 泄漏
直接访问旧模型(raw['STAT']、raw['AMT_UNIT']) STAT、AMT_UNIT 2 处
经 ACL 转成领域模型(c.frozen) 无 0 处

这不是 “代码风格”,而是可自动检查的边界:把 “领域层不许出现旧字段名” 写成 lint 规则,ACL 就真的挡住了;没有这条规则,三个月后又会有人为了 “临时取个数” 直接读旧表。

# 可复现:在领域层目录里搜旧字段名,命中即失败
grep -rn "CUST_NO\|AMT_UNIT\|STAT" ./domain/ && exit 1 || echo "ACL 边界完好"

反模式

反模式 后果
让新系统直接读旧库 旧模型渗透进新代码,ACL 名存实亡
ACL 里写业务规则 转换层变成第二套业务逻辑,无人敢改
绞杀者没有路由开关 出问题无法瞬间切回旧实现
双跑期不做数据对账 新旧结果不一致,切换时才发现
一次性重写 与业务节奏冲突,通常烂尾
不记录旧系统的“隐含规则” 迁移后才发现 “这个字段其实兼作状态机”

| ACL 只做字段改名,不做语义转换 | 旧系统的 “单位/枚举” 含义照样漏进领域层 | | 迁移后不删旧适配器 | 两套入口并存,新代码可能绕过 ACL |

三条铁律

🔑 旧系统的模型不许穿过边界;ACL 是唯一入口。

🔑 绞杀者必须有可一键回切的路由开关,并做过对账。

🔑 先补文档再迁移:旧系统的隐含规则不写下来,迁移就是猜。

自检

Q1 为什么 “直接读旧库” 是伪集成?

▸ 答案:因为它把旧系统的数据模型变成了新系统的编译期依赖:旧表改字段、改枚举、加触发器,新系统全部受影响;而且 “旧库里的隐含规则”(哪个字段兼作状态机)会悄悄渗进新逻辑。ACL 的价值就是把这种耦合挡在一个适配器里。

Q2 绞杀者模式的第一步应该做什么?

▸ 答案:先加路由开关与双跑对账,再迁第一个功能切片:让 “新实现” 与 “旧实现” 可并行、可对比、可回切。没有这层基础设施,迁移就是 “一次性切换”,风险与重写无异。

Q3 ACL 里为什么不能写业务规则?

▸ 答案:因为 ACL 的职责是翻译(字段映射、错误码转换、单位换算),一旦它开始 “判断该不该冻结、该不该退款”,就变成了第二个业务层:既没有领域模型的测试覆盖,又卡在所有调用的必经之路上。业务规则必须回到领域层。

Q4 什么时候该放弃绞杀、直接重写?

▸ 答案:当旧系统没有可切分的边界(一个巨大存储过程包办一切)、且业务允许长期停机或双轨运行时。即便如此,也应先做 “数据模型梳理 + 影子读” 验证。绝大多数 “直接重写” 失败在 “隐性规则没摸清”,不是技术。

Q5 双跑期怎么对账才有意义?

▸ 答案:按业务终态对账(订单状态、金额、数量),而不是逐字段比对:逐字段会被格式差异淹没。频率按业务风险定(资金类每小时,内容类每天),差异必须有 owner 与处理时限。

Q6 迁移完成后,旧系统还要保留多久?

▸ 答案:至少保留到 一个完整业务周期(含月结/季报/审计)之后,且确认没有 “只读调用” 残留。下线前要抓一次真实流量做空跑验证;直接关停是按 “代码里搜不到引用” 判断的,而引用常常在配置、脚本、报表里。

Q7 为什么 “领域层不许出现旧字段名” 这条 lint 比架构图有用?

▸ 答案:因为架构图不参与构建,而 lint 在 CI 里阻断合并:它把 “边界” 变成可执行规则,违规无法悄悄进主干。代价是要先冻结存量违规(白名单),再逐步清零,否则第一次接入就会因为历史代码太多而放弃(见 27.3 模块边界与依赖规则)。

故障复盘(典型模式,脱敏)

阶段 现象 判断 动作
上线 新系统金额比旧系统大 100 倍 旧库 amt_unit:1=元、2=分 停写、修数
T+1h 定位到新代码直接读了旧表字段 无 ACL,旧模型泄漏 加适配层
T+1d 修完数据,但报表仍用旧口径 两套口径并存 统一口径 + 对账
之后 加 lint:领域层禁出现旧字段名 边界可检查 逐模块迁移

根因:绕过 ACL 直接读旧表,且未处理 “单位” 语义。代价:金额错账 + 一天停写。根治:ACL 强制入口 + 领域层 lint + 双跑对账。

关联