引子:商場收銀軟體#
大鳥要小菜寫一個商場收銀軟體:營業員根據客戶購買商品的單價與數量,向客戶收費。小菜很快交出 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 <|-- ConcreteStrategyCabstract 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,原本的 CashSuper、CashNormal 等都不必更動:
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);比較兩種寫法:
- 簡單工廠:客戶端需要認識
CashSuper與CashFactory兩個類別- 策略 + 簡單工廠:客戶端只需認識
CashContext,耦合度更低
策略模式的優點#
- 演算法替換:所有演算法完成相同的工作,差異只在實現方式,能以相同方式呼叫,減少演算法類與使用者之間的耦合
- 抽取共通:繼承讓所有策略共享公共功能(例如
GetResult) - 單元測試:每個演算法都有獨立類別,能透過介面單獨測試
- 修改隔離:修改任一演算法不會影響其他演算法
- 消除條件語句:將不同行為封裝到獨立 Strategy 中,使用者不必再用
switch判斷
策略模式封裝了變化。
它不僅能封裝演算法,還能封裝幾乎任何類型的規則。只要在分析過程中聽到「需要在不同時間應用不同的業務規則」,就可以考慮使用策略模式。
仍未完美#
結合簡單工廠後,
CashContext中仍有switch:每新增一種促銷算法,仍需改CashContext。進一步的優化需要藉助反射(Reflection) 技術,本書在第 15 章「抽象工廠模式」中詳細介紹。