弃用与迁移
概述
代码是负债,不是资产。每一行代码都有持续的维护成本——要修的 bug、要更新的依赖、要打的安全补丁,以及要让新工程师上手。弃用是一门纪律:把那些不再值得保留的代码清除掉;迁移则是把用户安全地从旧方案迁到新方案的过程。
大多数工程组织擅长造东西,却很少擅长拆东西。本技能正是为了填补这一缺口。
适用场景
- 用新系统、API 或库替换旧的
- 下线不再需要的功能
- 合并重复的实现
- 清理没人负责但人人都在用的死代码
- 为新系统规划生命周期(弃用规划从设计阶段就应启动)
- 决定是继续维护遗留系统,还是投入资源进行迁移
核心原则
代码是负债
每一行代码都有持续成本:需要测试、文档、安全补丁、依赖更新,以及给周边任何工程师带来的心智负担。代码的价值在于它所提供的功能,而非代码本身。当同样的功能可以用更少的代码、更低的复杂度、或更好的抽象来实现时——旧代码就该退场。
海勒姆定律让移除变得困难
用户足够多时,每一个可观测的行为都会成为依赖——包括 bug、时序怪癖和未记录的副作用。这就是为什么弃用需要主动推动迁移,而非仅仅发个公告。当用户依赖的是新方案无法复现的行为时,他们无法"直接切换"。
弃用规划从设计阶段就应启动
构建新东西时,先问一句:"三年后我们怎么把它下掉?"接口清晰、有特性开关、暴露面最小化的系统,比那些到处泄露实现细节的系统更容易弃用。
弃用决策
在弃用任何东西之前,先回答这些问题:
1. 这个系统是否还在提供独特的价值?
→ 是,则继续维护。否,则继续往下走。
2. 有多少用户/调用方依赖它?
→ 量化迁移范围。
3. 是否有替代方案?
→ 没有,先建替代方案。没有替代就不要弃用。
4. 每个调用方的迁移成本是多少?
→ 能自动化的就做掉。手动且高成本的,与维护成本权衡。
5. 不弃用的持续维护成本是多少?
→ 安全风险、工程师时间、复杂度的机会成本。
建议式弃用与强制式弃用
| 类型 | 适用时机 | 实现机制 |
|---|---|---|
| 建议式 | 迁移可选,旧系统稳定 | 警告、文档、提示。用户按自己的节奏迁移。 |
| 强制式 | 旧系统有安全问题、阻塞进展、或维护成本不可持续 | 硬性截止日期。旧系统将在 X 日前下掉。需提供迁移工具。 |
默认采用建议式。 只有当维护成本或风险足以强制迁移时才用强制式。强制式弃用必须配套提供迁移工具、文档和支持——不能只公告一个截止日期。
迁移流程
步骤一:构建替代方案
没有可用的替代就不要弃用。替代方案必须满足:
- 覆盖旧系统的所有关键用例
- 具备文档与迁移指南
- 在生产中经过验证(不只是"理论上更好")
步骤二:公告与文档化
## 弃用通知:OldService
**状态:** 自 2025-03-01 起弃用
**替代方案:** NewService(参见下方迁移指南)
**下线日期:** 建议式——暂无硬性截止日期
**原因:** OldService 需要手动扩缩容,且缺乏可观测性。
NewService 可自动处理这两项。
### 迁移指南
1. 将 `import { client } from 'old-service'` 替换为 `import { client } from 'new-service'`
2. 更新配置(参见下方示例)
3. 运行迁移验证脚本: `npx migrate-check`
步骤三:增量迁移
一次一个调用方地迁移消费者,不要一次性全员迁移。每个调用方:
1. 识别与旧系统的所有接触点
2. 改用替代方案
3. 验证行为一致(测试、集成校验)
4. 移除对旧系统的引用
5. 确认无回归
流失守则(Churn Rule): 如果你拥有被弃用的基础设施,你就有责任帮用户迁移——或者提供向后兼容的更新,让用户无需迁移。不要发了弃用通知就把用户撂下,让他们自己想办法。
步骤四:移除旧系统
只有在所有调用方都迁移完之后:
1. 验证活跃用量为零(指标、日志、依赖分析)
2. 移除代码
3. 移除关联的测试、文档与配置
4. 移除弃用通知
5. 庆祝——删代码是一项成就
迁移模式
Strangler(绞杀者)模式
新旧系统并行运行,把流量逐步从旧切到新。当旧系统承担的流量降到 0% 时,移除之。
阶段一:新系统承担 0%,旧系统承担 100%
阶段二:新系统承担 10%(金丝雀)
阶段三:新系统承担 50%
阶段四:新系统承担 100%,旧系统空转
阶段五:移除旧系统
Adapter(适配器)模式
编写一个适配器,把对旧接口的调用翻译到新实现。消费者继续使用旧接口,后端由你慢慢迁移。
// 适配器:旧接口,新实现
class LegacyTaskService implements OldTaskAPI {
constructor(private newService: NewTaskService) {}
// 旧的方法签名,委托给新实现
getTask(id: number): OldTask {
const task = this.newService.findById(String(id));
return this.toOldFormat(task);
}
}
特性开关迁移
用特性开关把消费者从旧系统一个个切到新系统:
function getTaskService(userId: string): TaskService {
if (featureFlags.isEnabled('new-task-service', { userId })) {
return new NewTaskService();
}
return new LegacyTaskService();
}
僵尸代码
僵尸代码是那种没人负责、但人人都在用的代码。它无人主动维护,没有明确的负责人,持续积累安全漏洞与兼容性问题。常见征兆:
- 6 个月以上没有提交,但仍有活跃调用方
- 没有指定的维护人或团队
- 测试一直失败,无人修复
- 依赖存在已知漏洞,无人更新
- 文档引用的系统早已不复存在
应对方式: 要么指定负责人并妥善维护,要么带着具体迁移方案将其弃用。僵尸代码不能停留在悬而未决的状态——要么投入,要么下掉。
常见的自我安慰
| 自我安慰 | 现实 |
|---|---|
| "它还能跑,为啥要删?" | 没人维护的可用代码会持续积累安全债务与复杂度。维护成本在悄悄攀升。 |
| "万一以后有人需要呢" | 真到那时候再重建就行。"以防万一"地留着不用代码,比重建的成本更高。 |
| "迁移太贵了" | 把迁移成本和 2-3 年的持续维护成本对比一下,长期看迁移通常更便宜。 |
| "等新系统做完再弃用" | 弃用规划从设计阶段就该启动。等新系统做完时,你又有新的优先级了。现在就规划。 |
| "用户会自己迁移的" | 他们不会。要么提供工具、文档和激励,要么你自己动手迁移(流失守则)。 |
| "两套系统可以一直并行" | 两套干同一件事的系统意味着双倍的维护、测试、文档与上手成本。 |
红色信号
- 已弃用的系统没有替代方案
- 弃用公告没有迁移工具或文档
- "软"弃用挂了多年仍是建议式,毫无进展
- 僵尸代码没有负责人,却仍有活跃调用方
- 在已弃用的系统上添加新功能(应该把投入放在替代方案上)
- 弃用前未度量当前用量
- 未验证零活跃调用方就贸然删代码
验收清单
完成一次弃用后:
- 替代方案已通过生产验证,并覆盖所有关键用例
- 迁移指南存在,具备具体步骤与示例
- 所有活跃调用方都已迁移(通过指标/日志验证)
- 旧代码、测试、文档与配置已彻底移除
- 代码库中没有任何对已弃用系统的引用
- 弃用通知已移除(它已完成了使命)
使用边界
- 仅当任务与上游来源及本地项目上下文明显匹配时使用本技能。
- 在应用变更前,验证命令、生成的代码、依赖、凭据以及外部服务的行为。
- 不要把示例当成环境特定测试、安全审查、或对破坏性/高成本操作的用户审批的替代品。