軟體設計

Swift 策略模式(Strategy Pattern):使用協定與閉包替換演算法

你是否也遇過每種付款方式都讓 if-else 不斷增加的程式碼?

閱讀 4 分鐘
Swift 策略模式(Strategy Pattern):使用協定與閉包替換演算法 封面圖

你是否也遇過每種付款方式都讓 if-else 不斷增加的程式碼?

當排序選項、折扣計算與篩選邏輯開始糾結在同一個巨大的 switch 陳述式中,程式碼就會變得越來越不敢修改。

這時就該拿出 Swift 策略模式了。

今天會從實務角度,說明如何透過協定與閉包,像替換零件一樣替換演算法。

先掌握核心重點。

策略模式將「做什麼」與「如何做」分離,讓演算法可以從外部替換。

在 Swift 中,可以用協定(較重量級)或閉包(較輕量)兩種方式實作。

以下會逐一介紹。


什麼是策略模式?先用 3 行總結

先掌握骨架,再談複雜理論。

  1. 將要替換的演算法統一到一個共同規格(協定)中
  2. 將實際行為分別實作為策略物件或閉包
  3. 使用端只需要知道規格,不必了解具體內容

可以把它想成替遊戲角色更換武器。

角色只要知道「攻擊」即可,不論是劍、弓或魔法,都由裝備的武器自行處理。

不用修改角色程式碼,只替換武器。

這個「替換」幾乎就是策略模式的全部。


使用協定實作(標準方式)

先來看看最教科書式的協定做法。

以折扣計算為例,情境包含一般會員、VIP 會員與套用優惠券等多種計算方式。

以下程式碼定義所有策略都必須遵守的共同規格,以及一個實際策略。

// 所有折扣策略都必須遵守的共同規格
protocol DiscountStrategy {
    func discount(for price: Int) -> Int
}

// VIP 策略: 20% 折扣
struct VIPDiscount: DiscountStrategy {
    func discount(for price: Int) -> Int { price * 20 / 100 }
}

接著看看持有並使用這個策略的部分。

使用端物件不在乎傳入的是哪個策略。

Checkout 只知道規格,不知道內部實作
Checkout 只知道規格,不知道內部實作
struct Checkout {
    var strategy: DiscountStrategy   // 可以替換策略的位置

    func finalPrice(_ price: Int) -> Int {
        price - strategy.discount(for: price)
    }
}

// 在付款時只替換策略
let cart = Checkout(strategy: VIPDiscount())

即使新增折扣政策,也不需要修改 Checkout

只要新增一個遵循 DiscountStrategy 的結構即可。

不碰既有程式碼就能擴充功能,這是實務上最有感的優點。


使用閉包實作(Swift 風格)

不過策略明明很簡單,卻每次都建立結構,確實有點麻煩。

連簡單邏輯都全部包進協定,反而容易讓程式碼變得冗長。

這時在 Swift 中,閉包會更輕量也更自然。

struct Checkout {
    // 將策略本身當作函式接收
    var discount: (Int) -> Int

    func finalPrice(_ price: Int) -> Int {
        price - discount(price)
    }
}

// 直接在原處定義策略
let cart = Checkout(discount: { $0 * 20 / 100 })

不必宣告額外型別,傳入的同時就能直接寫入邏輯。

對於排序條件、篩選條件與簡單轉換等輕量演算法替換,這種方式清爽得多。

sorted(by:) 原來也是我們每天使用的策略模式
sorted(by:) 原來也是我們每天使用的策略模式

實際上,標準函式庫的 sorted(by:) 正是這種閉包策略模式。

排序的骨架固定,只透過閉包替換比較基準。

也就是說,我們早就在每天使用它了。


協定與閉包,什麼時候該用哪個?

以下整理了我在實務上的選擇標準。

分類 協定方式 閉包方式
適用情境 策略複雜或持有狀態 策略短小且簡單
可重複使用性 容易在多處重複使用 適合一次性使用
程式碼量 需要宣告型別,較多 直接定義,較少
測試 容易以個別型別驗證 簡單驗證邏輯即可
可讀性 透過名稱表達意圖 短時清楚,長時難讀

我的判斷標準如下。

如果策略具有內部狀態、會在多個畫面重複使用,或需要透過名稱清楚表達意圖,就適合使用協定。

反過來,如果只是隨時替換一兩行的邏輯,閉包就是正確選擇。

混合使用兩者完全沒問題。我會將大型政策交給協定,細部選項則使用閉包。


常見問題(Q&A)

Q. 策略模式和繼承(覆寫)有什麼不同?

繼承會綁定在父類別上,因此行為在編譯時就固定了。

相較之下,策略模式可以在執行期間自由替換策略,也不受類別階層限制。

Q. 和使用 enum 的 switch 陳述式處理有什麼不同?

使用 switch 時,每增加一個新案例,就必須開啟並修改既有程式碼。

策略模式只需要「新增」策略,就能減少修改既有程式碼的情況。


策略模式的核心其實只有一個:將會變動的部分移到外部,讓它容易替換。

靈活運用今天學到的協定與閉包,就能擺脫令人厭倦的 if-else 地獄。先從小段程式碼開始重構吧!

延伸閱讀