軟體設計

Swift Template Method Pattern:使用協定擴充建立骨架

你是否曾在不同類別中複製貼上相似邏輯,心想「這好像不太對」?在 iOS App 裡,當你第三次複製各畫面的資料載入程式碼時,手往往就會停下來。

閱讀 4 分鐘
Swift Template Method Pattern:使用協定擴充建立骨架 封面圖

你是否曾在不同類別中複製貼上相似邏輯,心想「這好像不太對」?在 iOS App 裡,當你第三次複製各畫面的資料載入程式碼時,手往往就會停下來。

這時可以派上用場的工具,就是 Template Method Pattern。

簡單說,這是一種把整體流程(骨架)固定在同一處,只讓各實作填入不同步驟的模式。在 Swift 中,使用協定擴充比繼承更簡潔。

本文將依序說明 Template Method Pattern 的概念、為什麼在 Swift 中協定擴充更接近理想解法,以及如何用實際程式碼建立骨架。

先看重點摘要

  1. Template Method Pattern 將「固定流程+可替換步驟」分離
  2. 傳統上透過父類別繼承實作,但在 Swift 中使用協定擴充更合適
  3. 把骨架放在 extension 的預設實作中,將差異部分保留為協定需求
  4. 不再受限於單一繼承與高耦合等繼承問題

什麼是 Template Method Pattern?

名稱聽起來很宏大,但概念其實很簡單。

想像一下料理食譜。「準備食材 → 處理 → 烹調 → 擺盤」的順序是固定的。

但食材與烹調方式會因料理而異。

其中順序(骨架)就是 Template Method,而每道料理不同的步驟就是可替換部分。

在同一處控制流程,只把細節實作委派出去。

這一句話幾乎就概括了 Template Method Pattern 的全部。

用程式碼來看,情況是這樣的。每個畫面的資料載入流程都相同:開始載入 → 請求 → 解析 → 更新畫面。

不同的只有「要請求哪個 URL」以及「如何解析收到的資料」。

每次重新撰寫這段共用流程,就是浪費。


Swift 中為什麼不用繼承,而是使用協定擴充?

原本的做法是在父類別中放入骨架,再由子類別 override 特定方法。就是 Java 教科書中的那種方式。

但在 Swift 中用類別繼承實作,會遇到不少限制。

  • 類別只能單一繼承。如果已經有一個父類別,就無法再繼承其他父類別
  • 完全不能套用在 struct 或 enum 上
  • 父子類別緊密耦合,之後很難替換

協定擴充能解決其中大部分問題。

將骨架方法透過 extension 的預設實作提供,並只把不同步驟宣告為協定需求。

如此一來,不論是 struct 或 class,只要採用該協定,就能免費取得共用骨架,不受繼承限制。

以下是用協定定義資料載入流程的範例。load()是固定骨架,其餘兩個是可替換部分。

protocol DataLoadable {
    associatedtype Item
    var endpoint: URL { get }          // 各畫面不同
    func parse(_ data: Data) -> [Item] // 各畫面不同
}

extension DataLoadable {
    func load() async throws -> [Item] {   // 固定骨架
        let (data, _) = try await URLSession.shared.data(from: endpoint)
        return parse(data)
    }
}

load()的順序由 extension 控制。

各畫面只要依自己的方式填入endpoint和parse即可。

骨架放在協定中,空白部分由採用端填入
骨架放在協定中,空白部分由採用端填入
固定骨架,只替換不同的步驟
固定骨架,只替換不同的步驟

實際建立骨架吧

現在來看看採用這個協定的一方。這不是繼承而是採用,因此也能使用 struct。

struct ArticleLoader: DataLoadable {
    let endpoint = URL(string: "https://api.example.com/articles")!
    func parse(_ data: Data) -> [Article] {
        (try? JSONDecoder().decode([Article].self, from: data)) ?? []
    }
}
// 使用: let articles = try await ArticleLoader().load()

如你所見,ArticleLoader並沒有直接實作load()。

即使如此,仍然可以呼叫load(),因為 extension 提供了預設實作。

這就是使用協定擴充建立 Template Method 的力量。新增畫面時,只要再建立一個只填入endpoint和parse的 struct 即可。

改用這種方式後,我把新增畫面的程式碼減少到一半以下。載入與錯誤處理邏輯集中在同一處後,除錯也容易許多。

只要再建立一個 struct 就能新增畫面,程式碼大幅減少
只要再建立一個 struct 就能新增畫面,程式碼大幅減少

有一點要注意。協定擴充的預設實作與 override 的行為略有不同。即使採用型別定義了同名方法,只要該方法沒有宣告為協定需求,就可能因靜態分派而呼叫到非預期的實作。

因此,最好務必將可替換的步驟宣告在協定本文中,作為協定需求。


簡短 Q&A 總結

Q. 這樣一來,繼承式的 Template Method 就不用了嗎?

在 UIKit 這種既有類別階層的地方,透過繼承 override 有時仍然很自然。請依情況選擇。

Q. associatedtype 讓人有點負擔。

如果回傳型別固定,可以不用 associatedtype,直接定義成一般協定。只有需要泛型時才使用它。


試著用協定擴充建立骨架後,會整理得如此清楚,甚至讓人覺得以前和繼承奮戰有點徒勞。

如果現在有一段持續複製貼上的邏輯,今天就挑一段抽成協定吧。很快就會抓到感覺。加油!

延伸閱讀