軟體設計

Swift 適配器模式(Adapter Pattern):用協定包裝舊版 API

適配器模式會將舊版 API 的介面轉換成新程式碼所期待的協定。本篇整理如何在 Swift 中建立轉換邊界、簡化替換與測試,以及它和其他包裝模式的差異。

閱讀 4 分鐘
Swift 適配器模式(Adapter Pattern):用協定包裝舊版 API 封面圖

你是否曾被舊版 API 程式碼困住:刪不掉,直接用也很痛苦?

把老舊網路模組接到新畫面時,這是任何人都會遇到的障礙。

先說結論,最乾淨的解法是用協定(interface)包裝舊版 API,並在兩者之間放入適配器。

適配器模式是在兩個不相容的介面之間插入「轉換器」。

如果你容易把它和同樣包裝物件的 Facade、Proxy、Decorator 搞混,可以在 4 種包裝模式比較 先從目的區分它們。

今天就依照我親自走過的流程,整理如何在 Swift 中套用這個模式。


本文可以學到的三件事

先為忙碌的讀者整理重點。

  1. 先用協定定義新程式碼需要的形式
  2. 建立符合該協定、負責轉換舊版 API 的適配器型別
  3. 讓畫面與檢視模型只相依於協定,而不是舊版 API

只要遵守這三點,之後整個替換 API 時,需要修改的範圍就會大幅縮小。

改成這種結構後,我覺得撰寫測試程式碼方便多了。


為什麼需要 Swift 適配器模式?

舊版 API 通常不是我們想要的樣子。

它可能以回呼為基礎、參數混亂,或回傳型別模糊。

如果新程式碼被迫配合老舊 API,等它消失的那天,整個程式碼庫都會受到影響。

所以我們會在中間放入適配器。

假設舊模組長得像下面這樣。

// 難以下手的舊版程式碼 API (以回呼為基礎)
class LegacyUserAPI {
    func fetch(id: Int,
               done: @escaping (NSDictionary?) -> Void) {
        // 老舊的網路呼叫...
    }
}

如果把 NSDictionary 原封不動帶到畫面層,之後更換這個 API 時,連畫面程式碼都得全部拆掉重寫。

一看就知道不想直接接上的樣子。


如何用協定包裝舊版 API(3 個步驟)

實際的包裝流程比想像中簡單。

步驟 1:用協定定義想要的形式。

先寫下新程式碼希望能以「這種方式呼叫」的形式。

// 新程式碼需要的簡潔介面
protocol UserRepository {
    func user(id: Int) async throws -> User
}

我把回呼改成 async/await,並將 NSDictionary 改成 User 型別。

步驟 2:讓適配器將舊版 API 調整為符合這個協定。

所有複雜的轉換都封裝在適配器內。

// 將舊版 API 轉換成新協定的適配器
struct LegacyUserAdapter: UserRepository {
    let legacy = LegacyUserAPI()
    func user(id: Int) async throws -> User {
        try await withCheckedThrowingContinuation { cont in
            legacy.fetch(id: id) { dict in
                cont.resume(returning: User(dict))
            }
        }
    }
}

在這裡,將回呼轉成 asyncwithCheckedThrowingContinuation 扮演核心角色。

步驟 3:讓畫面只相依於協定。

檢視模型不需要知道 LegacyUserAPI,只要知道 UserRepository 就可以了。

如此一來,舊版程式碼只會在適配器這一個地方出現。

舊版 API 藏在適配器後方,畫面只看得到協定
舊版 API 藏在適配器後方,畫面只看得到協定
畫面程式碼只要知道這個簡潔的協定即可
畫面程式碼只要知道這個簡潔的協定即可

使用適配器後有什麼不同?(與直接呼叫比較)

我用表格比較了直接呼叫與適配器方式。

項目 直接呼叫舊版 API 用適配器包裝
替換 API 時的修改範圍 整個畫面 適配器一處
單元測試 困難 用 mock 輕鬆完成
新程式碼可讀性
初期工作量 稍微增加

初期工作量確實會稍微增加。

但我完全不覺得這筆成本浪費。

測試時只要放入一個實作 UserRepository 的假物件,就能在沒有網路的情況下驗證畫面邏輯。

即使實際 API 尚未完成,也能先開始開發。

只要記住這三欄結構,就等於完成一半了
只要記住這三欄結構,就等於完成一半了

常見問題(Q&A)

Q. 適配器應該做成 struct 還是 class?

如果沒有狀態,使用 struct 就足夠了。

如果必須持續持有舊版物件,或需要共享參考,請使用 class

Q. 適配器模式和 Facade 模式有什麼不同?

適配器的目的是「讓介面相容」。

Facade 的目的是「將多個複雜元件以一個簡單介面呈現」,因此方向略有不同。

Q. 協定名稱該怎麼命名?

不要沿用舊版名稱,請從新程式碼的角度,依照所需角色命名。

例如使用 UserRepository,而不是 LegacyUserAPI


不要勉強刪除舊版程式碼,先用協定與適配器將它安靜地隔離起來。

只要把舊程式碼關在一個地方,接下來的重構就會輕鬆許多。也試著先包裝專案中最混亂的一個 API 吧。

延伸閱讀