API 改版後,舊程式還能使用嗎?這個問題需要先說清楚「怎麼使用」與「哪些行為必須保留」。 程式可能照常編譯,卻得到不同意思的結果;訊息也可能成功解析,卻無法完成原本的工作。
本文接續〈介面、契約與責任〉與 〈輸入、輸出與失敗語意〉,說明如何判斷改變的影響, 以及版本與棄用如何協助使用者遷移,不預設 API 透過 HTTP 提供。
查證日期:2026-11-02。官方實例採 Java SE 25、SemVer 2.0.0, 以及查證當日的 Protobuf proto3 指南與 Python PEP 387;後兩者是持續更新的文件。 本文只查閱文件,未執行程式或服務測試;自創例子皆為教學推演,不是實驗結果或產品政策。
相容性要說明比較的層次
相容性(compatibility)不是一個涵蓋所有情況的開關。 討論某次改變時,至少要區分下列問題:
| 層次 | 要確認的問題 |
|---|---|
| 原始碼相容性(source compatibility) | 舊呼叫端的原始碼不修改,改用新版介面後,是否仍能通過編譯或相應的語言檢查? |
| 二進位相容性(binary compatibility) | 已編譯的舊程式不重新編譯,是否仍能與新版元件連結?具體保證取決於平台的 ABI 與政策 |
| 訊息格式相容性(wire compatibility) | 依舊規則編碼的訊息,新接收端能否解析;依新規則編碼的訊息,舊接收端又能否解析? |
| 行為相容性 | 上述形式能接上之後,輸入、結果、失敗及副作用是否仍符合使用者可依賴的契約? |
ABI(Application Binary Interface,應用程式二進位介面)涉及編譯後元件互動的規則, 不是原始碼中的函式宣告本身。Wire compatibility 則著重編碼後的訊息, 也適用於之後才讀取的序列化資料,不只是在網路上即時交換的資料。 訊息契約還包含欄位的意義與使用規則,不能只檢查能否解碼。
Java SE 25 的 JLS §13.2 將二進位相容性界定為:原本能無錯誤連結的既有二進位程式, 改變後仍能無錯誤連結;這不等於所有執行結果都不變。 同章 §13.4.23 說明,新增多載方法可保持二進位相容, 但舊原始碼重新編譯時,可能因無法選出最適用的方法而失敗。 因此「舊編譯檔仍可連結」不能推出「舊原始碼仍可編譯」。 參考 JLS SE 25 §13.2 與§13.4.23。
這些是 Java 的具體規則,其他語言與平台須讀自己的保證。 對純粹交換訊息的服務,未必有二進位連結問題;對程序內函式庫,也未必有訊息編碼問題。
相容性有方向,也有比較對象
本文說「向後相容(backward compatible)」時,是指新版仍支援約定範圍內的舊用法。 但只寫這四個字仍不足夠,最好直接列出新舊組合與需要保留的行為:
| 組合 | 要確認的事 |
|---|---|
| 舊呼叫端 → 新提供者 | 舊用法是否仍被接受,並取得符合舊契約的結果? |
| 新呼叫端 → 舊提供者 | 新端是否會使用舊端沒有的能力;若會,如何辨識或避免? |
| 舊格式資料 → 新讀取端 | 新端能否讀懂既有資料,並保留其原本意義? |
| 新格式資料 → 舊讀取端 | 舊端能否處理新增內容,或至少依約處理不認識的資料? |
在訊息系統中,呼叫端送出輸入,同時也接收結果,所以請求與回應都要各自檢查。 新版能讀舊資料,並不表示舊版也能讀新資料;支援舊用法,也不表示舊端會自動獲得新能力。 若新舊版本會並存,或系統可能改回舊版,這些組合便會影響演進方式。
用原本的標題契約判斷行為改變
沿用上一篇的自創教學契約:
設定清單標題(title) → 成功(舊標題, 新標題) 或 失敗(標題無效)
舊規則接受 1 到 20 個 Unicode 碼位的字串,保留所有文字與空白; 有效時設定標題並回傳舊值與新值,無效時回傳失敗且清單不變。 未提供、空值及非字串也屬於已約定的失敗。以下各項改變分別對照這套舊規則:
| 假設的改變 | 對舊契約的影響 |
|---|---|
| 把長度上限從 20 縮為 10 | 11 到 20 個碼位原本應成功,現在卻失敗;破壞舊有效輸入的保證 |
| 設定前先移除前後空白 | 例如「 讀書 」原本必須完整保留,現在儲存成「讀書」;即使仍成功,結果與狀態也已改變 |
| 成功時只回傳新標題 | 舊呼叫端原本可取得舊標題,現在無法取得;改變結果契約 |
| 無效標題改成另一種失敗結果 | 若舊端依約辨識「標題無效」,新結果會改變其錯誤處理路徑 |
| 把長度上限從 20 放寬為 30 | 舊有效輸入可以保持原行為,但 21 到 30 個碼位原本明訂失敗,現在變為成功;並非完整保留舊契約 |
破壞性變更(breaking change)要相對於已承諾支援的用法與行為判斷。 它不只包括刪除入口或修改型別;結果意義、錯誤種類與狀態影響也可能是破壞點。 放寬輸入是否可接受,取決於承諾範圍;本例已明訂超長輸入的失敗行為,不能當作從未保證。
也不是每項實作改變都會破壞契約。例如改用另一種內部結構儲存標題, 若在支援範圍內仍保留所有承諾,便可以改變實作而維持這些行為。 至於「修正 bug」,仍應檢查原契約與實際影響;這個名稱本身不會證明相容。
新增欄位為何也需要判斷
前面的偽碼沒有指定訊息格式,不能直接推定結果可以任意新增欄位。 假設現在另行設計一種物件格式,使用具名欄位表示舊標題與新標題,再新增「是否改變」欄位。 舊接收端能否繼續使用,至少取決於契約是否允許未知欄位、解析器如何處理, 以及新欄位缺席時,新接收端是否仍能處理舊提供者的結果。 若舊契約要求拒絕未知欄位,新增欄位就可能使舊端拒絕整個結果。
Protobuf proto3 指南把新增欄位列為其二進位訊息格式的 wire-safe 改變, 同時提醒 wire-safe 仍可能破壞應用程式碼;該指南也指出 ProtoJSON 的演進規則不同。 例如新增列舉值,可能使採完整列舉分支的應用程式無法編譯。 不能把某種格式的規則擴大成「所有 API 加欄位都安全」。 參考 Protobuf proto3:Updating A Message Type。
因此,訊息可解析只是其中一層。接收端還要能正確解讀欄位、預設值與結果種類, 才能完成原本依賴的工作;兩端對同一個數字理解成不同單位,即使解碼成功也不夠。
版本號傳達承諾,不能代替驗證
版本號可以讓使用者辨識改變,但先要知道提供者採用什麼版本政策,以及公開 API 的範圍。
SemVer 2.0.0 是一套可採用的規則,不是所有產品都必須遵守的共同政策。
依其規範,採用者須宣告公開 API;在 1.0.0 之後,以 MAJOR.MINOR.PATCH 表達:
| 變動 | SemVer 2.0.0 的規則摘要 |
|---|---|
| PATCH,修補版本 | 只有向後相容的錯誤修正 |
| MINOR,次版本 | 新增向後相容的功能;標記公開 API 棄用也必須提升次版本 |
| MAJOR,主版本 | 對公開 API 引入不向後相容的改變 |
SemVer 另將 0.y.z 視為初始開發階段,公開 API 不應被視為穩定。
其 FAQ 建議在主版本移除功能前,至少先有一個包含棄用標記的次版本。
參考 SemVer 2.0.0 規範與棄用說明。
版本號是提供者對改變的分類與承諾訊號,無法在技術上阻止違約或遺漏。
使用者仍要閱讀變更說明、適用的支援政策,並驗證自己依賴的契約。
看到 2.4.1 這種外形,也不能直接認定該產品採用 SemVer。
棄用是遷移訊號,移除是另一個動作
棄用(deprecation)表示提供者已不建議繼續依賴某項能力,通常需要說明替代方式或後續安排。 被標記棄用,不等於已經刪除;是否安排移除、何時移除,以及如何通知,都要讀該產品政策。 例如 Python PEP 387 區分一般棄用與 soft deprecation:後者仍保留文件與測試, 不再發展該能力,但不意味已排定移除。這是 Python 的政策,不能套用到整個產業。 參考 PEP 387:Soft Deprecation。
對前述標題例子,如果真的需要改成移除空白的規則,演進時應先交代改變理由、受影響輸入與替代用法。 可以考慮提供新操作或明確選項,讓新舊行為在遷移期間並存,再驗證呼叫端是否已改用新規則。 移除舊行為前,需確認公告的版本安排、使用者遷移與支援政策;本文不為專案設定固定期限。 PEP 387 也提供先討論、發出警告、等待與收集回饋,最後才改變或移除的具體流程; 其時程與例外規則只適用於該政策範圍。 參考 PEP 387:Making Incompatible Changes。
判斷一次演進時,先寫出舊契約、改變項目、新舊組合與需要保留的結果, 再選擇版本與遷移安排。這樣才能把「應該相容」轉成可檢查的具體主張。 如何記錄這些規則,見文件與契約描述;如何設計檢查,見 API 測試。
相關文件
- 介面、契約與責任:確認公開承諾與雙方責任。
- 輸入、輸出與失敗語意:查看標題例子的完整舊契約。
- API 共通知識索引:查看閱讀順序與相關主題。