弃用与迁移
概览
代码是一种负债,不是资产。每一行代码都有持续维护成本:要修 bug、要升级依赖、要打安全补丁、还要让新工程师看懂。弃用,是把已经不再值得维护的代码移除出去的纪律;迁移,则是把用户安全地从旧东西带到新东西上的过程。
大多数工程组织都很擅长构建新东西,但很少真正擅长删除旧东西。这个 skill 就是为了解决这个缺口。
何时使用
- 用新系统、API 或库替换旧系统
- 下线一个已经不再需要的功能
- 合并重复实现
- 移除“没人负责但所有人都依赖”的死而不僵代码
- 为新系统规划完整生命周期,也就是在设计阶段就考虑弃用
- 判断是继续维护遗留系统,还是投入迁移
核心原则
代码是负债
每一行代码都有持续成本:需要测试、需要文档、需要安全补丁、需要依赖升级,也会增加附近所有人的心智负担。代码的价值不在“它存在”,而在它提供的功能。如果同样的功能可以用更少的代码、更低的复杂度或更好的抽象提供出来,那么旧代码就应该被移除。
Hyrum's Law 让移除变难
只要用户足够多,系统中每一个可观察行为都会被依赖上,包括 bug、时序怪癖和没写文档的副作用。这就是为什么弃用不能只发公告,还必须做主动迁移。因为用户依赖的往往不只是“功能名义上该做什么”,还包括替代品未必复刻的那些细节。
弃用规划应该从设计时开始
在构建一个新系统时,就该问一句:“如果 3 年后要把它删掉,该怎么删?”
有清晰接口、feature flag 和小暴露面的系统,远比到处泄露实现细节的系统更容易弃用。
做弃用决策前要回答什么
在弃用任何东西前,先回答下面的问题:
1. Does this system still provide unique value?
→ If yes, maintain it. If no, proceed.
2. How many users/consumers depend on it?
→ Quantify the migration scope.
3. Does a replacement exist?
→ If no, build the replacement first. Don't deprecate without an alternative.
4. What's the migration cost for each consumer?
→ If trivially automated, do it. If manual and high-effort, weigh against maintenance cost.
5. What's the ongoing maintenance cost of NOT deprecating?
→ Security risk, engineer time, opportunity cost of complexity.
强制弃用 vs 建议弃用
| 类型 | 适用场景 | 机制 |
|---|---|---|
| Advisory | 迁移是可选的,旧系统仍稳定 | 警告、文档、提醒,由用户按自己的节奏迁 |
| Compulsory | 旧系统存在安全问题、阻碍进展,或维护成本已不可接受 | 明确截止时间,到期移除,并提供迁移工具 |
默认优先 Advisory。 只有当风险或维护成本足以 justify 强制迁移时,才用 Compulsory。并且强制弃用必须配套迁移工具、迁移文档和支持,不能只宣布一个最后期限。
迁移流程
步骤 1:先把替代品建出来
没有可用替代品,就不要宣布弃用。替代品至少要满足:
- 覆盖旧系统的所有关键用例
- 带有文档和迁移指南
- 已在生产中被验证,不是理论上“更好”
步骤 2:公告并文档化
## Deprecation Notice: OldService
**Status:** Deprecated as of 2025-03-01
**Replacement:** NewService (see migration guide below)
**Removal date:** Advisory — no hard deadline yet
**Reason:** OldService requires manual scaling and lacks observability.
NewService handles both automatically.
### Migration Guide
1. Replace `import { client } from 'old-service'` with `import { client } from 'new-service'`
2. Update configuration (see examples below)
3. Run the migration verification script: `npx migrate-check`
步骤 3:渐进式迁移
一个消费者一个消费者地迁,不要一把梭全量切。对每个消费者:
1. Identify all touchpoints with the deprecated system
2. Update to use the replacement
3. Verify behavior matches (tests, integration checks)
4. Remove references to the old system
5. Confirm no regressions
The Churn Rule: 如果你是被弃用基础设施的拥有者,那么迁移用户要么由你来做,要么你得提供向后兼容更新,让他们无需自己承担迁移成本。不要只发一条弃用公告,然后把问题留给用户自己琢磨。
步骤 4:移除旧系统
只有当所有消费者都已迁完,才能真正删旧系统:
1. Verify zero active usage (metrics, logs, dependency analysis)
2. Remove the code
3. Remove associated tests, documentation, and configuration
4. Remove the deprecation notices
5. Celebrate — removing code is an achievement
迁移模式
绞杀者模式(Strangler Pattern)
让旧系统和新系统并行运行,逐步把流量从旧切到新。当旧系统流量降到 0% 时,再删它。
Phase 1: New system handles 0%, old handles 100%
Phase 2: New system handles 10% (canary)
Phase 3: New system handles 50%
Phase 4: New system handles 100%, old system idle
Phase 5: Remove old system
适配器模式(Adapter Pattern)
做一个适配器,把旧接口调用翻译成新实现。这样消费者可以继续用旧接口,而你在底层完成迁移。
// Adapter: old interface, new implementation
class LegacyTaskService implements OldTaskAPI {
constructor(private newService: NewTaskService) {}
// Old method signature, delegates to new implementation
getTask(id: number): OldTask {
const task = this.newService.findById(String(id));
return this.toOldFormat(task);
}
}
Feature Flag 迁移
用 feature flag 逐个消费者切换:
function getTaskService(userId: string): TaskService {
if (featureFlags.isEnabled('new-task-service', { userId })) {
return new NewTaskService();
}
return new LegacyTaskService();
}
僵尸代码
Zombie code 指的是“没人拥有,但大家都依赖”的代码。它没人认真维护、也没有清晰 owner,却不断积累安全漏洞和兼容性问题。典型信号:
- 6 个月以上没有提交,但仍有活跃使用者
- 没有明确维护者或归属团队
- 有失败测试,但没人修
- 依赖存在已知漏洞,但没人更新
- 文档还在提一些已经不存在的系统
处理方式: 要么分配 owner 并认真维护,要么给出具体迁移计划并弃用。Zombie code 不能一直卡在暧昧地带,要么继续投资,要么彻底移除。
常见自我安慰
| 自我安慰 | 现实 |
|---|---|
| “它还在工作,为什么要删?” | 没人维护但还能跑的代码,会悄悄积累安全债和复杂度,维护成本只会越来越高。 |
| “以后也许还会用到” | 真要用到,以后可以重建。为了“万一”保留无用代码,通常比将来重建更贵。 |
| “迁移成本太高了” | 应该拿迁移成本去对比未来 2-3 年的维护成本,长期看迁移通常更便宜。 |
| “等新系统做完再考虑弃用” | 弃用规划应在设计时就开始。等新系统做完时,你往往已经被新优先级裹走了。 |
| “用户会自己迁的” | 他们不会。你得给工具、给文档、给激励,或者你自己把迁移做掉。 |
| “两个系统同时维护也没问题” | 两套做同一件事的系统,意味着双倍的维护、测试、文档和 onboarding 成本。 |
危险信号
- 旧系统被标记弃用,却根本没有替代品
- 宣布弃用,但没有迁移工具或文档
- “软弃用”挂了好几年都没有进展
- Zombie code 既没 owner,又仍被使用
- 已弃用系统上还继续加新功能
- 弃用前没测当前用量
- 在没验证“零活跃消费者”的情况下删代码
验证
完成一轮弃用后,确认:
- 替代品已经在生产中被验证,并覆盖所有关键用例
- 迁移指南存在,步骤具体且带示例
- 所有活跃消费者都已迁移,可通过指标或日志证明
- 旧代码、测试、文档和配置都已完整移除
- 代码库中不再有对旧系统的引用
- 弃用通知也已移除,它们的使命已经完成