deprecation-and-migration

deprecation-and-migration

热门

管理弃用和迁移。在移除旧系统、API或功能时使用。在将用户从一个实现迁移到另一个实现时使用。在决定是维护还是淘汰现有代码时使用。

7.8万Star
8330Fork
更新于 2026/7/12
SKILL.md
readonly只读
name
deprecation-and-migration
description

管理弃用和迁移。在移除旧系统、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

  1. 扩展。 添加 full_name 为可空。部署。(旧代码忽略它;没有破坏。)
  2. 双写。 应用在每次插入/更新时同时写入 namefull_name。部署。
  3. 回填。name → full_name 复制到现有行,分批进行,以免锁定表。
  4. 切换读取。 将应用指向 full_name,继续写入两者。部署并稳定运行。
  5. 收缩。 停止写入 name,然后在后续单独的部署中删除该列。

每一步都是独立可部署和可逆的:如果步骤4出现问题,回滚代码,而 full_name 仍然被填充。将每个阶段视为一个薄垂直切片——参见 incremental-implementation 技能。

规则:

  • 先添加,后破坏,破坏单独进行。 添加(新的可空列、新表、新索引)在任何部署中都是安全的;删除和重命名在没有代码引用旧结构之后单独部署。
  • 每个迁移都有经过测试的回退路径。 无法回退的迁移意味着无法回滚的部署。在合并之前编写并运行 down
  • 分批回填,远离热路径。 对数百万行执行单个 UPDATE 会锁定表;分块并限速。
  • 在不阻塞写入的情况下构建大索引(例如 Postgres 的 CREATE INDEX CONCURRENTLY)。
  • 通过功能开关与代码解耦,当切换有风险时,正如上面的功能开关迁移模式所示。

僵尸代码

僵尸代码是无人拥有但人人依赖的代码。它没有积极维护,没有明确的所有者,并积累安全漏洞和兼容性问题。迹象:

  • 6个月以上没有提交,但有活跃的消费者
  • 没有分配维护者或团队
  • 无人修复的失败测试
  • 具有已知漏洞但无人更新的依赖
  • 引用已不存在系统的文档

应对措施: 要么分配所有者并妥善维护,要么制定具体的迁移计划进行弃用。僵尸代码不能处于悬而未决的状态——它要么获得投资,要么被移除。

常见合理化借口

合理化借口 现实
“它还能工作,为什么要移除?” 无人维护的工作代码会积累安全债务和复杂性。维护成本悄然增长。
“以后可能有人需要它” 如果以后需要,可以重建。保留未使用的代码“以防万一”的成本高于重建。
“迁移成本太高” 将迁移成本与2-3年的持续维护成本进行比较。迁移通常长期更便宜。
“我们会在完成新系统后弃用它” 弃用规划从设计时开始。等到新系统完成时,你会有新的优先级。现在就开始规划。
“用户会自己迁移” 他们不会。提供工具、文档和激励——或者自己进行迁移(搅动规则)。
“我们可以无限期地维护两个系统” 两个做同样事情的系统意味着双倍的维护、测试、文档和入职成本。
“只需重命名列,就一行代码” 在发布期间,旧代码和新代码同时运行——其中一个会查询一个不再存在的列。使用扩展/收缩,永远不要原地重命名。
“我会在同一个迁移中添加列并删除旧列” 这将安全的添加与破坏性的删除耦合在一起。删除操作在没有代码引用旧结构之后单独部署。
“如果需要,我们会编写回退” 没有回退路径的迁移意味着无法回滚的部署。在合并之前编写并运行 down

红旗警示

  • 弃用的系统没有可用的替代品
  • 弃用公告没有迁移工具或文档
  • “软”弃用已经建议多年但没有进展
  • 没有所有者但有活跃消费者的僵尸代码
  • 向已弃用的系统添加新功能(应投资于替代品)
  • 弃用前未测量当前使用情况
  • 在未验证零活跃消费者的情况下移除代码
  • 模式变更和依赖它的代码在同一个部署中发布
  • 列被原地重命名或删除,而不是通过扩展/收缩
  • 迁移合并时没有经过测试的回退路径,或者回填锁定了表

验证

完成弃用后:

  • [ ] 替代品已在生产环境中验证,并覆盖所有关键用例
  • [ ] 存在包含具体步骤和示例的迁移指南
  • [ ] 所有活跃消费者已迁移(通过指标/日志验证)
  • [ ] 旧代码、测试、文档和配置已完全移除
  • [ ] 代码库中没有对弃用系统的引用
  • [ ] 弃用通知已移除(它们已完成使命)

完成数据库模式迁移后:

  • [ ] 变更以增量阶段发布(扩展→回填→收缩),而不是单次原地编辑
  • [ ] 在每个部署步骤中,旧代码和新代码对模式都有效
  • [ ] 每个迁移都有经过测试的回退路径;回填以限速批次运行
  • [ ] 破坏性步骤(删除/重命名)在没有代码引用旧结构之后单独部署