管理弃用和迁移。在移除旧系统、API或功能时使用。在将用户从一个实现迁移到另一个实现时使用。在决定是维护还是淘汰现有代码时使用。
弃用与迁移
概述
代码是负债,而非资产。每一行代码都有持续的维护成本——修复漏洞、更新依赖、应用安全补丁、以及让新工程师上手。弃用是移除不再有价值的代码的纪律,而迁移是将用户安全地从旧系统转移到新系统的过程。
大多数工程组织擅长构建东西,但很少擅长移除它们。本技能旨在填补这一空白。
何时使用
- 用新系统、API或库替换旧的
- 淘汰不再需要的功能
- 合并重复的实现
- 移除无人拥有但人人依赖的死代码
- 规划新系统的生命周期(弃用规划从设计时开始)
- 决定是维护遗留系统还是投资迁移
核心原则
代码是负债
每一行代码都有持续的成本:需要测试、文档、安全补丁、依赖更新以及任何接触它的人的心智负担。代码的价值在于它提供的功能,而非代码本身。当相同的功能可以用更少的代码、更低的复杂度或更好的抽象来实现时——旧代码应该被移除。
海勒姆定律使移除变得困难
只要有足够多的用户,每一个可观察的行为都会被依赖——包括bug、时序怪癖和未记录的副作用。这就是为什么弃用需要主动迁移,而不仅仅是公告。当用户依赖的行为在替代品中无法复现时,他们无法“直接切换”。
弃用规划从设计时开始
在构建新东西时,问自己:“三年后我们如何移除它?”设计时具有清晰接口、功能开关和最小表面积(surface area)的系统比那些到处泄露实现细节的系统更容易弃用。
弃用决策
在弃用任何东西之前,回答以下问题:
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. 确认没有回归
搅动规则: 如果你拥有被弃用的基础设施,你有责任迁移你的用户——或者提供无需迁移的向后兼容更新。不要宣布弃用后让用户自己解决。
第四步:移除旧系统
只有在所有消费者都迁移之后:
1. 验证零活跃使用(指标、日志、依赖分析)
2. 移除代码
3. 移除相关的测试、文档和配置
4. 移除弃用通知
5. 庆祝——移除代码是一项成就
迁移模式
扼杀者模式
并行运行旧系统和新系统。逐步将流量从旧系统路由到新系统。当旧系统处理0%的流量时,移除它。
阶段1:新系统处理0%,旧系统处理100%
阶段2:新系统处理10%(金丝雀)
阶段3:新系统处理50%
阶段4:新系统处理100%,旧系统空闲
阶段5:移除旧系统
适配器模式
创建一个适配器,将旧接口的调用转换为新实现。消费者继续使用旧接口,同时你迁移后端。
// 适配器:旧接口,新实现
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();
}
数据库模式迁移(扩展/收缩)
模式变更是最危险的迁移,因为数据是你无法通过回滚部署来恢复的东西。失败模式是将模式变更与代码变更耦合:在同一个发布中重命名列并开始使用新名称,在发布窗口期间——当旧代码和新代码同时运行时——其中一个会查询一个不存在的列。解决方法是永远不要原地更改列。以增量阶段迁移,使得旧代码和新代码在每个步骤中都有效。
扩展 ──────────────→ 迁移 ──────────────→ 收缩
添加新列,可空, 回填现有行, 一旦没有代码读取旧列,
与旧列并存 应用双写旧+新 在后续单独的部署中删除它
工作示例——将 name 重命名为 full_name:
- 扩展。 添加
full_name为可空。部署。(旧代码忽略它;没有破坏。) - 双写。 应用在每次插入/更新时同时写入
name和full_name。部署。 - 回填。 将
name → full_name复制到现有行,分批进行,以免锁定表。 - 切换读取。 将应用指向
full_name,继续写入两者。部署并稳定运行。 - 收缩。 停止写入
name,然后在后续单独的部署中删除该列。
每一步都是独立可部署和可逆的:如果步骤4出现问题,回滚代码,而 full_name 仍然被填充。将每个阶段视为一个薄垂直切片——参见 incremental-implementation 技能。
规则:
- 先添加,后破坏,破坏单独进行。 添加(新的可空列、新表、新索引)在任何部署中都是安全的;删除和重命名在没有代码引用旧结构之后单独部署。
- 每个迁移都有经过测试的回退路径。 无法回退的迁移意味着无法回滚的部署。在合并之前编写并运行
down。 - 分批回填,远离热路径。 对数百万行执行单个
UPDATE会锁定表;分块并限速。 - 在不阻塞写入的情况下构建大索引(例如 Postgres 的
CREATE INDEX CONCURRENTLY)。 - 通过功能开关与代码解耦,当切换有风险时,正如上面的功能开关迁移模式所示。
僵尸代码
僵尸代码是无人拥有但人人依赖的代码。它没有积极维护,没有明确的所有者,并积累安全漏洞和兼容性问题。迹象:
- 6个月以上没有提交,但有活跃的消费者
- 没有分配维护者或团队
- 无人修复的失败测试
- 具有已知漏洞但无人更新的依赖
- 引用已不存在系统的文档
应对措施: 要么分配所有者并妥善维护,要么制定具体的迁移计划进行弃用。僵尸代码不能处于悬而未决的状态——它要么获得投资,要么被移除。
常见合理化借口
| 合理化借口 | 现实 |
|---|---|
| “它还能工作,为什么要移除?” | 无人维护的工作代码会积累安全债务和复杂性。维护成本悄然增长。 |
| “以后可能有人需要它” | 如果以后需要,可以重建。保留未使用的代码“以防万一”的成本高于重建。 |
| “迁移成本太高” | 将迁移成本与2-3年的持续维护成本进行比较。迁移通常长期更便宜。 |
| “我们会在完成新系统后弃用它” | 弃用规划从设计时开始。等到新系统完成时,你会有新的优先级。现在就开始规划。 |
| “用户会自己迁移” | 他们不会。提供工具、文档和激励——或者自己进行迁移(搅动规则)。 |
| “我们可以无限期地维护两个系统” | 两个做同样事情的系统意味着双倍的维护、测试、文档和入职成本。 |
| “只需重命名列,就一行代码” | 在发布期间,旧代码和新代码同时运行——其中一个会查询一个不再存在的列。使用扩展/收缩,永远不要原地重命名。 |
| “我会在同一个迁移中添加列并删除旧列” | 这将安全的添加与破坏性的删除耦合在一起。删除操作在没有代码引用旧结构之后单独部署。 |
| “如果需要,我们会编写回退” | 没有回退路径的迁移意味着无法回滚的部署。在合并之前编写并运行 down。 |
红旗警示
- 弃用的系统没有可用的替代品
- 弃用公告没有迁移工具或文档
- “软”弃用已经建议多年但没有进展
- 没有所有者但有活跃消费者的僵尸代码
- 向已弃用的系统添加新功能(应投资于替代品)
- 弃用前未测量当前使用情况
- 在未验证零活跃消费者的情况下移除代码
- 模式变更和依赖它的代码在同一个部署中发布
- 列被原地重命名或删除,而不是通过扩展/收缩
- 迁移合并时没有经过测试的回退路径,或者回填锁定了表
验证
完成弃用后:
- [ ] 替代品已在生产环境中验证,并覆盖所有关键用例
- [ ] 存在包含具体步骤和示例的迁移指南
- [ ] 所有活跃消费者已迁移(通过指标/日志验证)
- [ ] 旧代码、测试、文档和配置已完全移除
- [ ] 代码库中没有对弃用系统的引用
- [ ] 弃用通知已移除(它们已完成使命)
完成数据库模式迁移后:
- [ ] 变更以增量阶段发布(扩展→回填→收缩),而不是单次原地编辑
- [ ] 在每个部署步骤中,旧代码和新代码对模式都有效
- [ ] 每个迁移都有经过测试的回退路径;回填以限速批次运行
- [ ] 破坏性步骤(删除/重命名)在没有代码引用旧结构之后单独部署






