你是否也遇過每種付款方式都讓 if-else 不斷增加的程式碼?
當排序選項、折扣計算與篩選邏輯開始糾結在同一個巨大的 switch 陳述式中,程式碼就會變得越來越不敢修改。
這時就該拿出 Swift 策略模式了。
今天會從實務角度,說明如何透過協定與閉包,像替換零件一樣替換演算法。
先掌握核心重點。
策略模式將「做什麼」與「如何做」分離,讓演算法可以從外部替換。
在 Swift 中,可以用協定(較重量級)或閉包(較輕量)兩種方式實作。
以下會逐一介紹。
什麼是策略模式?先用 3 行總結
先掌握骨架,再談複雜理論。
- 將要替換的演算法統一到一個共同規格(協定)中
- 將實際行為分別實作為策略物件或閉包
- 使用端只需要知道規格,不必了解具體內容
可以把它想成替遊戲角色更換武器。
角色只要知道「攻擊」即可,不論是劍、弓或魔法,都由裝備的武器自行處理。
不用修改角色程式碼,只替換武器。
這個「替換」幾乎就是策略模式的全部。
使用協定實作(標準方式)
先來看看最教科書式的協定做法。
以折扣計算為例,情境包含一般會員、VIP 會員與套用優惠券等多種計算方式。
以下程式碼定義所有策略都必須遵守的共同規格,以及一個實際策略。
// 所有折扣策略都必須遵守的共同規格
protocol DiscountStrategy {
func discount(for price: Int) -> Int
}
// VIP 策略: 20% 折扣
struct VIPDiscount: DiscountStrategy {
func discount(for price: Int) -> Int { price * 20 / 100 }
}
接著看看持有並使用這個策略的部分。
使用端物件不在乎傳入的是哪個策略。
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:) 正是這種閉包策略模式。
排序的骨架固定,只透過閉包替換比較基準。
也就是說,我們早就在每天使用它了。
協定與閉包,什麼時候該用哪個?
以下整理了我在實務上的選擇標準。
| 分類 | 協定方式 | 閉包方式 |
|---|---|---|
| 適用情境 | 策略複雜或持有狀態 | 策略短小且簡單 |
| 可重複使用性 | 容易在多處重複使用 | 適合一次性使用 |
| 程式碼量 | 需要宣告型別,較多 | 直接定義,較少 |
| 測試 | 容易以個別型別驗證 | 簡單驗證邏輯即可 |
| 可讀性 | 透過名稱表達意圖 | 短時清楚,長時難讀 |
我的判斷標準如下。
如果策略具有內部狀態、會在多個畫面重複使用,或需要透過名稱清楚表達意圖,就適合使用協定。
反過來,如果只是隨時替換一兩行的邏輯,閉包就是正確選擇。
混合使用兩者完全沒問題。我會將大型政策交給協定,細部選項則使用閉包。
常見問題(Q&A)
Q. 策略模式和繼承(覆寫)有什麼不同?
繼承會綁定在父類別上,因此行為在編譯時就固定了。
相較之下,策略模式可以在執行期間自由替換策略,也不受類別階層限制。
Q. 和使用 enum 的 switch 陳述式處理有什麼不同?
使用 switch 時,每增加一個新案例,就必須開啟並修改既有程式碼。
策略模式只需要「新增」策略,就能減少修改既有程式碼的情況。
策略模式的核心其實只有一個:將會變動的部分移到外部,讓它容易替換。
靈活運用今天學到的協定與閉包,就能擺脫令人厭倦的 if-else 地獄。先從小段程式碼開始重構吧!

