搜尋文件

輸入關鍵字,搜尋標題與正文。

    API文件 03/16

    相容性與演進

    本文提要

    API 改版後,舊程式還能使用嗎?這個問題需要先說清楚「怎麼使用」與「哪些行為必須保留」。 程式可能照常編譯,卻得到不同意思的結果;訊息也可能成功解析,卻無法完成原本的工作。

    文件與本文目錄
    本主題文件

    API 改版後,舊程式還能使用嗎?這個問題需要先說清楚「怎麼使用」與「哪些行為必須保留」。 程式可能照常編譯,卻得到不同意思的結果;訊息也可能成功解析,卻無法完成原本的工作。

    本文接續〈介面、契約與責任〉與 〈輸入、輸出與失敗語意〉,說明如何判斷改變的影響, 以及版本與棄用如何協助使用者遷移,不預設 API 透過 HTTP 提供。

    查證日期:2026-11-02。官方實例採 Java SE 25、SemVer 2.0.0, 以及查證當日的 Protobuf proto3 指南與 Python PEP 387;後兩者是持續更新的文件。 本文只查閱文件,未執行程式或服務測試;自創例子皆為教學推演,不是實驗結果或產品政策。

    相容性要說明比較的層次

    相容性(compatibility)不是一個涵蓋所有情況的開關。 討論某次改變時,至少要區分下列問題:

    表格・2 欄 4 列
    層次要確認的問題
    原始碼相容性(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)」時,是指新版仍支援約定範圍內的舊用法。 但只寫這四個字仍不足夠,最好直接列出新舊組合與需要保留的行為:

    表格・2 欄 4 列
    組合要確認的事
    舊呼叫端 → 新提供者舊用法是否仍被接受,並取得符合舊契約的結果?
    新呼叫端 → 舊提供者新端是否會使用舊端沒有的能力;若會,如何辨識或避免?
    舊格式資料 → 新讀取端新端能否讀懂既有資料,並保留其原本意義?
    新格式資料 → 舊讀取端舊端能否處理新增內容,或至少依約處理不認識的資料?

    在訊息系統中,呼叫端送出輸入,同時也接收結果,所以請求與回應都要各自檢查。 新版能讀舊資料,並不表示舊版也能讀新資料;支援舊用法,也不表示舊端會自動獲得新能力。 若新舊版本會並存,或系統可能改回舊版,這些組合便會影響演進方式。

    用原本的標題契約判斷行為改變

    沿用上一篇的自創教學契約:

    TEXT
    設定清單標題(title) → 成功(舊標題, 新標題) 或 失敗(標題無效)
    

    舊規則接受 1 到 20 個 Unicode 碼位的字串,保留所有文字與空白; 有效時設定標題並回傳舊值與新值,無效時回傳失敗且清單不變。 未提供、空值及非字串也屬於已約定的失敗。以下各項改變分別對照這套舊規則:

    表格・2 欄 5 列
    假設的改變對舊契約的影響
    把長度上限從 20 縮為 1011 到 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 表達:

    表格・2 欄 3 列
    變動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 測試。

    相關文件