本章主軸#
本章引入新的案例研究:跨國電子商務(e-commerce)系統,並用它來介紹策略模式(Strategy)。Strategy 是書中第一個示範「以模式應對問題領域變動」的範例。第 16 章「分析矩陣」會延續同一案例。
為什麼要為改變而設計#
短期偷懶常常造成長期災難——換機油、桌面當資料夾、軟體開發都一樣。許多專案以下列藉口忽略長期維護:
- 「不知道未來會怎麼變」
- 「想清楚會卡住分析無止盡」
- 「沒預算」「客戶在催」「以後再說」
結果只剩兩種選項:「分析癱瘓(paralysis by analysis)」或「逃船而出(abandon by ship date)」。
真相是:為改變而設計,往往並沒有比較貴——而且可讀、可測、可改的程式碼能補回所有時間成本。
GoF 的三條核心原則:
- 面向介面而非實作來設計
- 偏好聚合勝於繼承
- 思考設計中應該變動的部分——把會變的概念封裝起來
案例:跨國 e-commerce 訂單系統#

Figure 9-1: e-commerce 系統中的銷售單架構——TaskController 把請求轉給 SalesOrder
TaskController接收訂單請求,交給SalesOrderSalesOrder負責填單、稅金計算、列印收據- 新需求:要能處理美國以外的稅務規則
處理新需求的常見做法#
| 做法 | 缺點 |
|---|---|
| Copy & Paste | 兩份程式碼分裂,維護成本暴增 |
| switch / if | 易引發「switch creep」——多個 switch 共享同一變數,新增情境時得改多處 |
| 函式指標 / delegate | 無法承載狀態,能力受限 |
| 繼承 | 多軸變動下會類別爆炸 |
| 委派給新物件(Strategy) | 變動隔離、可獨立擴展、可獨立測試 |
switch creep 的真實樣貌#
當需要處理多國家、多語系、多日期格式時,switch 會散落各處:
switch (myNation) {
case US: /* US tax */ break;
case Canada: /* CA tax */ break;
}
switch (myNation) {
case US: /* US currency */ break;
case Canada: /* CA currency */ break;
}加入「魁北克講法文」時,switch 還得往內巢狀。新增情境要找出每個 switch、補對位置——這正是 switch creep。
用繼承「特化」也救不了#
例如為加拿大訂單衍生 CanadianSalesOrder、覆寫稅金邏輯。但稅、語言、日期、運費都各自變動時,繼承樹會爆炸並出現大量重複。
用 Strategy 重新切割#
依照 GoF 的兩步驟:
- 找出變動點並封裝:把稅務規則抽成
CalcTax抽象類別,USTax、CanTax各自實作 - 以聚合取代繼承:
SalesOrder持有CalcTax參考,由外部決定要用哪一種

Figure 9-4: 以聚合取代繼承——SalesOrder 持有抽象 CalcTax,依情境換用 USTax 或 CanTax
public abstract class CalcTax {
public abstract double taxAmount(long itemSold, double price);
}
public class SalesOrder {
public void process(CalcTax taxToUse) {
double tax = taxToUse.taxAmount(itemNumber, price);
}
}把變動移到自己的類別中,類似資料庫的「正規化」——把可能變動的欄位放進獨立資料表,再用 foreign key 連起來。
Strategy 的關鍵特性#
| 欄位 | 內容 |
|---|---|
| Intent | 依情境選用不同的商業規則或演算法 |
| Problem | 何種演算法該被套用,取決於 client 或資料 |
| Solution | 把演算法的「選擇」與「實作」分開 |
| Participants | Strategy 定義介面、ConcreteStrategy 實作、Context 持有並使用 |
| Consequences | 消除 switch;多個演算法須共用同一介面;Context 可能要新增 getter 給策略查詢 |
| Implementation | Context 持有 Strategy 抽象成員;衍生類別實作具體演算法 |
實務筆記#
商業規則 = 廣義的策略#
雖然 Strategy 名義上講的是「演算法」,但在實務中任何商業規則都適合用 Strategy 包裝。聽到「不同情境套不同規則」時就可以考慮。
如何把資訊傳給策略#
策略放到外部後,原本可直接存取的資料就要透過下列方式取得:
- 把所需參數傳進去
- 把 Customer 整個物件傳進去
- 把 Context 物件本身的參考傳進去
例:英國對特定年齡長者的食品免稅 → 三種做法都行,端看通用程度。
大幅降低單元測試成本#
每個演算法獨立成類,可單獨透過介面測試,不必管 Context 的前置條件,也不需擔心多個策略組合的爆炸問題。
何時把策略「裝進」Context#
如果一個 Context 物件只會用同一個策略,可在建構子裡把策略一次注入,呼叫時不必每次傳。Context 仍不知道實際型別,模式效力不變。

Figure 9-5: SalesOrder 透過 Configuration 物件得知該用哪一種 CalcTax
避免類別過多的小技巧#
C++:把 ConcreteStrategy 寫在抽象 strategy 的 header / cpp 中。Java:用 inner class。前提是你掌握所有策略,不需要外人擴充。
本章要記住的事#
- 為改變而設計通常不會更貴;不為改變設計才是真貴
- switch / 特化繼承在多軸變動下都會崩潰
- Strategy 把演算法封進獨立類別、用聚合取代繼承
- 它不只能裝演算法,更可承載任何商業規則
- Strategy 是測試友善的設計