Manages deprecation and migration. Use when removing old systems, APIs, or features. Use when migrating users from one implementation to another. Use when deciding whether to maintain or sunset existing code.
棄用與遷移
概述
程式碼是負債,不是資產。每一行程式碼都有持續的維護成本——要修的錯誤、要更新的依賴、要套用的安全性修補,以及要上手的新工程師。棄用是移除不再值得維護的程式碼的紀律,而遷移是將使用者安全地從舊系統轉移到新系統的過程。
大多數工程組織擅長建構東西,但很少擅長移除它們。這項技能正是為了解決這個落差。
使用時機
- 用新系統、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:
- 擴展。 新增
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。 |
紅旗
- 已棄用的系統沒有可用的替代方案
- 棄用公告沒有遷移工具或文件
- 「軟性」棄用多年來一直處於建議狀態,沒有任何進展
- 無人擁有但有活躍消費者的殭屍程式碼
- 在已棄用的系統上新增功能(應投資於替代方案)
- 棄用時沒有測量目前使用情況
- 在未確認零活躍消費者的情況下移除程式碼
- 綱要變更與依賴它的程式碼在同一個部署中發布
- 資料行被原地重新命名或刪除,而不是透過擴展/收縮
- 遷移合併時沒有經過測試的回退路徑,或回填鎖定了資料表
驗證
完成棄用後:
- [ ] 替代方案已在生產環境中驗證,並涵蓋所有關鍵使用案例
- [ ] 遷移指南存在,包含具體步驟和範例
- [ ] 所有活躍消費者都已遷移(透過指標/日誌驗證)
- [ ] 舊程式碼、測試、文件和設定已完全移除
- [ ] 程式碼庫中沒有對棄用系統的引用
- [ ] 棄用通知已移除(它們已完成任務)
完成資料庫綱要遷移後:
- [ ] 變更以增量階段發布(擴展 → 回填 → 收縮),而不是單一的原地編輯
- [ ] 新舊程式碼在每個部署步驟中都對綱要有效
- [ ] 每個遷移都有經過測試的回退路徑;回填以限速批次執行
- [ ] 破壞性步驟(刪除/重新命名)在沒有程式碼引用舊結構之後的獨立部署中發布






