本章主軸#

本章引入新的案例研究:跨國電子商務(e-commerce)系統,並用它來介紹策略模式(Strategy)。Strategy 是書中第一個示範「以模式應對問題領域變動」的範例。第 16 章「分析矩陣」會延續同一案例。

為什麼要為改變而設計#

短期偷懶常常造成長期災難——換機油、桌面當資料夾、軟體開發都一樣。許多專案以下列藉口忽略長期維護:

  • 「不知道未來會怎麼變」
  • 「想清楚會卡住分析無止盡」
  • 「沒預算」「客戶在催」「以後再說」

結果只剩兩種選項:「分析癱瘓(paralysis by analysis)」或「逃船而出(abandon by ship date)」。

真相是:為改變而設計,往往並沒有比較貴——而且可讀、可測、可改的程式碼能補回所有時間成本。

GoF 的三條核心原則:

  • 面向介面而非實作來設計
  • 偏好聚合勝於繼承
  • 思考設計中應該變動的部分——把會變的概念封裝起來

案例:跨國 e-commerce 訂單系統#

Figure 9-1: e-commerce 系統中的銷售單架構——TaskController 把請求轉給 SalesOrder

  • TaskController 接收訂單請求,交給 SalesOrder
  • SalesOrder 負責填單、稅金計算、列印收據
  • 新需求:要能處理美國以外的稅務規則

處理新需求的常見做法#

做法缺點
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 的兩步驟:

  1. 找出變動點並封裝:把稅務規則抽成 CalcTax 抽象類別,USTaxCanTax 各自實作
  2. 以聚合取代繼承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把演算法的「選擇」與「實作」分開
ParticipantsStrategy 定義介面、ConcreteStrategy 實作、Context 持有並使用
Consequences消除 switch;多個演算法須共用同一介面;Context 可能要新增 getter 給策略查詢
ImplementationContext 持有 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 是測試友善的設計