🎯 什麼情境該想到我
當你「同一件事有好幾種可互換的做法」的時候——排序規則、計費方案、壓縮/加密演算法、驗證策略、重試退避。
★ 在 Go 和 TS 裡,它常常退化成「一個函式型別」——這是本頁最重要的觀察。
⚙️ 怎麼用
意圖
定義一系列演算法,把每一個都封裝起來,並使它們可以互相替換。策略模式讓演算法可以獨立於使用它的客戶端而變化。
它要消滅的是這個:
// ❌ 每加一種計費方式就要改這個函式
func Price(kind string, base float64) float64 {
switch kind {
case "normal": return base
case "vip": return base * 0.8
case "student": return base * 0.5
... // 無限成長
}
}★ Go 的長相:多數情況下「一個 func 型別」就夠了
// ★ 這就是 Strategy
type PricingStrategy func(base float64) float64
var (
Normal PricingStrategy = func(b float64) float64 { return b }
VIP PricingStrategy = func(b float64) float64 { return b * 0.8 }
Student PricingStrategy = func(b float64) float64 { return b * 0.5 }
)
type Checkout struct{ pricing PricingStrategy } // Context
func (c Checkout) Total(base float64) float64 {
return c.pricing(base)
}★ GoF 需要
Strategy介面 + 個ConcreteStrategy類別,是因為 C++/Java 沒有頭等函式。
Go 有,所以整個類別階層消失了。
什麼時候仍然該用介面而非函式:
| 用函式 | 用介面 |
|---|---|
| 策略只有一個行為 | ★ 策略有多個相關方法(Encode/Decode、Compress/Decompress) |
| 無狀態 | ★ 需要狀態(連線、快取、計數器) |
| 不需要名字 | ★ 需要 Name()、String() 之類的元資訊 |
// 需要多方法時:
type Codec interface {
Encode(io.Writer, any) error
Decode(io.Reader, any) error
ContentType() string
}標準庫裡的 Strategy
| 案例 | 策略是什麼 |
|---|---|
slices.SortFunc(s, cmp) | ★ cmp func(a, b E) int 就是排序策略 |
http.Client{Transport: ...} | RoundTripper 是傳輸策略 |
slog.New(handler) | Handler 是輸出策略(也是 橋接模式) |
tls.Config.GetCertificate | 憑證選擇策略 |
TypeScript 的長相
type PricingStrategy = (base: number) => number;
const strategies = {
normal: (b: number) => b,
vip: (b: number) => b * 0.8,
student: (b: number) => b * 0.5,
} satisfies Record<string, PricingStrategy>; // ★ satisfies 保留字面量型別又檢查形狀
type Tier = keyof typeof strategies; // "normal" | "vip" | "student"
const total = (tier: Tier, base: number) => strategies[tier](base);★
satisfies+keyof typeof是 TS 的關鍵組合(見 工具-satisfies與型別標註):
它讓Tier自動從實作推導出來——加一個策略,型別自動跟著更新,不用維護兩份清單。
這是 Go 做不到的(Go 得手動維護 enum 與 map)。
★ Strategy 與三個孿生模式的區分
| 差別 | |
|---|---|
| 狀態模式 | ★ 結構完全相同。策略是「外部主動挑一種」;狀態是「內部隨狀態自動切換,且狀態自己會轉換」 |
| 橋接模式 | Bridge 的抽象那側有自己的階層,且通常建構時決定;Strategy 常在執行期換 |
| 範本方法模式 | 範本方法換的是步驟(繼承);策略換的是整個演算法(組合) |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- ★ 只有兩三種且幾乎不變 → 直接
switch就好。
引入策略要多一個型別、多一層間接、多一個「誰決定用哪個」的問題。
判準:這些分支會不會被獨立地加/改?會 → 策略;不會 → switch。 - ★ 選擇的責任被推給呼叫端。呼叫端得知道有哪些策略、差在哪。
→ 常需要再配一個 工廠方法模式 把「字串 → 策略」的對應收攏起來。 - 策略之間必須真的可互換。簽章一樣但語義不同(一個同步一個非同步、一個會 panic 一個不會)
就不是策略,只是長得像。 - 函式型策略不好除錯:堆疊裡是匿名函式。給它們具名(
var VIP PricingStrategy = vipPricing)。 - Go 的函式型策略無法比較(
func不可比較),也無法當 map key。需要辨識就用介面 +Name()。 - 不要把策略當成「所有 if 的解藥」。有些條件分支就是業務邏輯,拆成策略只會讓流程散落各處、更難讀。
🔗 相關工具
- 狀態模式 —— 結構孿生,意圖不同
- 範本方法模式 —— 繼承版的對照
- 橋接模式 —— 兩個維度時的升級版
- 工廠方法模式 —— 收攏「選哪個策略」的責任
- 工具-優先組合而非繼承 —— 策略是它最直接的體現
- 工具-封裝變化點 —— 把會變的演算法封裝起來
- 工具-satisfies與型別標註 —— TS 的策略表寫法
- 設計模式