deprecation-and-migration

deprecation-and-migration

熱門

管理棄用與遷移。用於移除舊系統、API 或功能時。用於將使用者從一個實作遷移到另一個時。用於決定要維護或淘汰現有程式碼時。

7.8萬星標
8330分支
更新於 2026/7/12
SKILL.md
唯讀
名稱
deprecation-and-migration
描述

管理棄用與遷移。用於移除舊系統、API 或功能時。用於將使用者從一個實作遷移到另一個時。用於決定要維護或淘汰現有程式碼時。

棄用與遷移

概述

程式碼是負債,不是資產。每一行程式碼都有持續的維護成本——要修的錯誤、要更新的依賴、要套用的安全性修補,以及要上手的新工程師。棄用是移除不再值得維護的程式碼的紀律,而遷移是將使用者安全地從舊系統轉移到新系統的過程。

大多數工程組織擅長建構東西,但很少擅長移除它們。這項技能正是為了解決這個落差。

使用時機

  • 用新系統、API 或函式庫取代舊的
  • 淘汰不再需要的功能
  • 合併重複的實作
  • 移除無人擁有但大家都依賴的死亡程式碼
  • 規劃新系統的生命週期(棄用規劃應從設計階段開始)
  • 決定要維護舊系統還是投資遷移

核心原則

程式碼是負債

每一行程式碼都有持續成本:需要測試、文件、安全性修補、依賴更新,以及任何接近它的人的心智負擔。程式碼的價值在於它提供的功能,而不是程式碼本身。當相同的功能可以用更少的程式碼、更低的複雜度或更好的抽象來提供時——舊的程式碼就該移除。

Hyrum 定律讓移除變得困難

當使用者夠多時,每一個可觀察的行為都會被依賴——包括錯誤、時序問題和未記載的副作用。這就是為什麼棄用需要主動遷移,而不只是公告。當使用者依賴於替代品沒有複製的行為時,他們無法「直接切換」。

棄用規劃應從設計階段開始

在建構新東西時,問自己:「3 年後我們要如何移除這個?」設計時採用乾淨介面、功能開關和最小表面積的系統,比那些到處洩漏實作細節的系統更容易棄用。

棄用決策

在棄用任何東西之前,先回答這些問題:

1. 這個系統是否仍提供獨特價值?
   → 如果是,就維護它。如果否,繼續。

2. 有多少使用者/消費者依賴它?
   → 量化遷移範圍。

3. 是否有替代方案存在?
   → 如果沒有,先建立替代方案。不要在沒有替代方案的情況下棄用。

4. 每個消費者的遷移成本是多少?
   → 如果可以自動化且成本低,就做。如果是手動且高成本,則與維護成本權衡。

5. 如果不棄用,持續的維護成本是多少?
   → 安全性風險、工程師時間、複雜度的機會成本。

建議性 vs 強制性棄用

類型 使用時機 機制
建議性 遷移是可選的,舊系統穩定 警告、文件、提示。使用者依自己的時間表遷移。
強制性 舊系統有安全性問題、阻礙進展或維護成本無法持續 硬性截止日期。舊系統將在 X 日期前移除。提供遷移工具。

預設使用建議性。 只有在維護成本或風險足以證明強制遷移合理時,才使用強制性。強制性棄用需要提供遷移工具、文件和支援——你不能只是宣布一個截止日期。

遷移流程

步驟 1:建立替代方案

不要在沒有可行替代方案的情況下棄用。替代方案必須:

  • 涵蓋舊系統的所有關鍵使用案例
  • 有文件和遷移指南
  • 已在生產環境中驗證(不只是「理論上更好」)

步驟 2:公告與文件化

## 棄用通知:OldService

**狀態:** 自 2025-03-01 起已棄用
**替代方案:** NewService(請參閱下方遷移指南)
**移除日期:** 建議性——尚無硬性截止日期
**原因:** OldService 需要手動擴展且缺乏可觀測性。
            NewService 自動處理兩者。

### 遷移指南
1. 將 `import { client } from 'old-service'` 替換為 `import { client } from 'new-service'`
2. 更新設定(請參閱下方範例)
3. 執行遷移驗證腳本:`npx migrate-check`

步驟 3:增量遷移

一次遷移一個消費者,而不是全部同時。對於每個消費者:

1. 識別與棄用系統的所有接觸點
2. 更新為使用替代方案
3. 驗證行為是否匹配(測試、整合檢查)
4. 移除對舊系統的引用
5. 確認沒有回歸

轉移規則: 如果你擁有被棄用的基礎設施,你有責任遷移你的使用者——或提供不需要遷移的向後相容更新。不要宣布棄用後就讓使用者自己想辦法。

步驟 4:移除舊系統

只有在所有消費者都遷移之後:

1. 確認零活躍使用(指標、日誌、依賴分析)
2. 移除程式碼
3. 移除相關的測試、文件和設定
4. 移除棄用通知
5. 慶祝——移除程式碼是一項成就

遷移模式

Strangler 模式

同時執行新舊系統。逐步將流量從舊系統導向新系統。當舊系統處理 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

紅旗

  • 已棄用的系統沒有可用的替代方案
  • 棄用公告沒有遷移工具或文件
  • 「軟性」棄用多年來一直處於建議狀態,沒有任何進展
  • 無人擁有但有活躍消費者的殭屍程式碼
  • 在已棄用的系統上新增功能(應投資於替代方案)
  • 棄用時沒有測量目前使用情況
  • 在未確認零活躍消費者的情況下移除程式碼
  • 綱要變更與依賴它的程式碼在同一個部署中發布
  • 資料行被原地重新命名或刪除,而不是透過擴展/收縮
  • 遷移合併時沒有經過測試的回退路徑,或回填鎖定了資料表

驗證

完成棄用後:

  • [ ] 替代方案已在生產環境中驗證,並涵蓋所有關鍵使用案例
  • [ ] 遷移指南存在,包含具體步驟和範例
  • [ ] 所有活躍消費者都已遷移(透過指標/日誌驗證)
  • [ ] 舊程式碼、測試、文件和設定已完全移除
  • [ ] 程式碼庫中沒有對棄用系統的引用
  • [ ] 棄用通知已移除(它們已完成任務)

完成資料庫綱要遷移後:

  • [ ] 變更以增量階段發布(擴展 → 回填 → 收縮),而不是單一的原地編輯
  • [ ] 新舊程式碼在每個部署步驟中都對綱要有效
  • [ ] 每個遷移都有經過測試的回退路徑;回填以限速批次執行
  • [ ] 破壞性步驟(刪除/重新命名)在沒有程式碼引用舊結構之後的獨立部署中發布