引子:商場收銀軟體#

大鳥要小菜寫一個商場收銀軟體:營業員根據客戶購買商品的單價與數量,向客戶收費。小菜很快交出 v1.0:兩個文字框、一個按鈕、列表框與標籤。

但需求隨即變動:

  • v1.1:商場活動,所有商品打八折
  • 接著還有打七折、打五折,甚至滿 300 返 100 的促銷算法

如果每次促銷都要改程式、重新部署,到所有機器上重裝一遍,那是噩夢。

用簡單工廠模式重構#

小菜將打折邏輯抽象為一個收費抽象類,再衍生具體子類:

abstract class CashSuper
{
    public abstract double acceptCash(double money);
}

class CashNormal : CashSuper
{
    public override double acceptCash(double money) => money;
}

class CashRebate : CashSuper
{
    private double moneyRebate;
    public CashRebate(string rebate) { moneyRebate = double.Parse(rebate); }
    public override double acceptCash(double money) => money * moneyRebate;
}

class CashReturn : CashSuper
{
    private double moneyCondition, moneyReturn;
    public CashReturn(string cond, string ret)
    {
        moneyCondition = double.Parse(cond);
        moneyReturn    = double.Parse(ret);
    }
    public override double acceptCash(double money)
    {
        if (money >= moneyCondition)
            return money - Math.Floor(money / moneyCondition) * moneyReturn;
        return money;
    }
}

再加上一個 CashFactory 工廠類,依字串條件回傳對應子類實例。

簡單工廠模式(Simple Factory)解決了「物件如何建立」的問題,但工廠裡仍包含所有收費方式

商場常常更改打折額度與返利規則,每次都得修改工廠類、重新編譯部署,仍然很糟糕。

策略模式登場#

策略模式(Strategy Pattern):定義一系列演算法,把它們各自封裝起來,並讓它們可以互相替換;此模式讓演算法的變化不會影響到使用演算法的客戶。[DP]

策略模式的關鍵洞察:

  • 打折、返利等都是演算法
  • 演算法本身就是一種策略
  • 演算法是會互相替換的——這正是「變化點」
  • 封裝變化點是物件導向重要的思維方式

基本結構#

  • Strategy(抽象策略):定義所有支援演算法的公共介面
  • ConcreteStrategy(具體策略):實作 Strategy 的具體演算法或行為
  • Context(上下文):維護一個對 Strategy 的引用,由 ConcreteStrategy 來配置
classDiagram
    class Context {
        -Strategy strategy
        +ContextInterface()
    }
    class Strategy {
        <<abstract>>
        +AlgorithmInterface()
    }
    class ConcreteStrategyA {
        +AlgorithmInterface()
    }
    class ConcreteStrategyB {
        +AlgorithmInterface()
    }
    class ConcreteStrategyC {
        +AlgorithmInterface()
    }
    Context o--> Strategy
    Strategy <|-- ConcreteStrategyA
    Strategy <|-- ConcreteStrategyB
    Strategy <|-- ConcreteStrategyC
abstract class Strategy
{
    public abstract void AlgorithmInterface();
}

class Context
{
    private Strategy strategy;
    public Context(Strategy strategy) { this.strategy = strategy; }
    public void ContextInterface() => strategy.AlgorithmInterface();
}

客戶端:

Context context = new Context(new ConcreteStrategyA());
context.ContextInterface();

商場收銀的策略版本#

只需新增一個 CashContext,原本的 CashSuperCashNormal 等都不必更動:

class CashContext
{
    private CashSuper cs;
    public CashContext(CashSuper csuper) { this.cs = csuper; }
    public double GetResult(double money) => cs.acceptCash(money);
}

策略結合簡單工廠#

純策略模式仍要在客戶端做 switch 判斷該選哪個策略。把判斷邏輯搬進 Context,用簡單工廠的方式實例化:

class CashContext
{
    private CashSuper cs = null;
    public CashContext(string type)
    {
        switch (type)
        {
            case "正常收費": cs = new CashNormal(); break;
            case "滿 300 返 100": cs = new CashReturn("300", "100"); break;
            case "打 8 折":  cs = new CashRebate("0.8"); break;
        }
    }
    public double GetResult(double money) => cs.acceptCash(money);
}

客戶端只需與 CashContext 互動:

CashContext csuper = new CashContext(cbxType.SelectedItem.ToString());
double total = csuper.GetResult(price * num);

比較兩種寫法:

  • 簡單工廠:客戶端需要認識 CashSuperCashFactory 兩個類別
  • 策略 + 簡單工廠:客戶端只需認識 CashContext,耦合度更低

策略模式的優點#

  • 演算法替換:所有演算法完成相同的工作,差異只在實現方式,能以相同方式呼叫,減少演算法類與使用者之間的耦合
  • 抽取共通:繼承讓所有策略共享公共功能(例如 GetResult
  • 單元測試:每個演算法都有獨立類別,能透過介面單獨測試
  • 修改隔離:修改任一演算法不會影響其他演算法
  • 消除條件語句:將不同行為封裝到獨立 Strategy 中,使用者不必再用 switch 判斷

策略模式封裝了變化。

它不僅能封裝演算法,還能封裝幾乎任何類型的規則。只要在分析過程中聽到「需要在不同時間應用不同的業務規則」,就可以考慮使用策略模式。

仍未完美#

結合簡單工廠後,CashContext 中仍有 switch:每新增一種促銷算法,仍需改 CashContext

進一步的優化需要藉助反射(Reflection) 技術,本書在第 15 章「抽象工廠模式」中詳細介紹。