【設計模式】策略模式——以商場促銷為例
本文內容參考自《大話設計模式》(程傑 著)
注:以下程式碼為java實現
版本1
需求:
做一個商場收銀軟體,營業員根據客戶所購買商品的單價和數量,向客戶收費。
關鍵程式碼:
public class Cash
{
private double total = 0;
public void submit(int num, double price)
{
double totalPrices = num * price;
total += totalPrices;
System.out .println("單價:" + price + " 數量:" + num + "合計:" + totalPrices);
}
public double getTotal()
{
return total;
}
public void setTotal(double total)
{
this.total = total;
}
}
版本2
需求:
增加打折功能
思路1:修改程式碼,比如打7折,則total *= 0.7;
評價:如果取消打折,或者修改折扣,需要頻繁修改程式碼,不推薦。
思路2:增加折扣選項,關鍵程式碼如下:
public class Cash
{
private double total = 0;
private int selectedIndex = 0;
public void selectFormLoad()
{
String[] selectForm = { "正常收費", "打8折", "打7折", "打5折" };
selectedIndex = 0;
}
public void submit(int num, double price)
{
double totalPrices = 0;
switch (selectedIndex)
{
case 0:
totalPrices = num * price;
break;
case 1:
totalPrices = num * price * 0.8;
break;
case 2:
totalPrices = num * price * 0.7;
break;
case 3:
totalPrices = num * price * 0.5;
break;
}
total += totalPrices;
System.out.println("單價:" + price + " 數量:" + num + "合計:" + totalPrices);
}
public double getTotal()
{
return total;
}
public void setTotal(double total)
{
this.total = total;
}
public int getSelectedIndex()
{
return selectedIndex;
}
public void setSelectedIndex(int selectedIndex)
{
this.selectedIndex = selectedIndex;
}
}
問題:重複的程式碼太多,而且選項少,可變性不高!
版本3
需求:
可以靈活修改折扣,並且可以返利
//現金收費介面
public interface CashSuper
{
public double acceptCash(double money);
}
//正常收費子類
public class CashNormal implements CashSuper
{
public double acceptCash(double money)
{
return money;
}
}
//打折收費子類
public class CashRebate implements CashSuper
{
private double moneyRebate = 1;
public CashRebate(double moneyRebate)
{
this.moneyRebate = moneyRebate;
}
public double acceptCash(double money)
{
return money * moneyRebate;
}
}
//返利收費子類
public class CashReturn implements CashSuper
{
private double moneyCondition = 0;
private double moneyReturn = 0;
public CashReturn(double moneyCondition, double moneyReturn)
{
this.moneyCondition = moneyCondition;
this.moneyReturn = moneyReturn;
}
public double acceptCash(double money)
{
double result = money;
if (money >= moneyCondition)
{
result = money - money / moneyCondition * moneyReturn;
}
return result;
}
}
//現金收費工廠類
public class CashFactory
{
public static CashSuper createCash(String type)
{
CashSuper cs = null;
if ("正常收費".equals(type))
{
cs = new CashNormal();
}
else if ("滿300返100".equals(type))
{
cs = new CashReturn(300, 100);
}
else if ("打8折".equals(type))
{
cs = new CashRebate(0.8);
}
return cs;
}
}
//客戶端程式碼
public class Main
{
private static double total = 0;
public static void main(String[] args)
{
consume("正常收費", 1, 1000);
consume("滿300返100", 1, 1000);
consume("打8折", 1, 1000);
System.out.println("總計:" + total);
}
public static void consume(String type, int num, double price)
{
CashSuper csuper = CashFactory.createCash(type);
double totalPrices = 0;
totalPrices = csuper.acceptCash(num * price);
total += totalPrices;
System.out.println("單價:" + price + " 數量:" + num + "合計:" + totalPrices);
}
}
以上程式碼進行了抽象封裝,並使用了簡單的工廠類,靈活性高了很多。
問題:簡單工廠模式只是解決物件的建立問題,而且由於工廠本身包括了所有的收費方式,商場是可能經常性地更改打折額度和返利額度的,每次維護或擴充套件收費方式都要改動這個工廠,以致程式碼需要重新編譯部署,這是非常糟糕的處理方式,所以用它不是最好的辦法。面對演算法的時常變動,我們可以使用策略模式!
版本4
需求:
可以經常性地更改打折額度和返利額度,而且要維護成本較低。
策略模式(Strategy):
它定義了演算法家族,分別封裝起來,讓它們之間可以互相替換,此模式讓演算法的變化,不會影響到使用演算法的客戶。
商場收銀時,如何促銷,用打折還是返利,其實都是一些演算法,最重要的是這些演算法是隨時都可能互相替換的,就這點變化,而封裝變化點是我們面向物件的一種很重要的思維方式。我們來看看策略模式的結構圖和基本程式碼:
//Strategy類,定義所有支援的演算法的公共介面
public interface Strategy
{
public void algorithmInterface();
}
//ConcreteStrategy封裝了具體的演算法或行為,繼承於Strategy
public class ConcreteStrategyA implements Strategy
{
public void algorithmInterface()
{
System.out.println("演算法A實現");
}
}
public class ConcreteStrategyB implements Strategy
{
public void algorithmInterface()
{
System.out.println("演算法A實現");
}
}
public class ConcreteStrategyC implements Strategy
{
public void algorithmInterface()
{
System.out.println("演算法C實現");
}
}
//Context用一個ConcreteStrategy來配置,維護一個對Strategy物件的引用
public class Context
{
private Strategy strategy;
public Context(Strategy strategy)
{
this.strategy = strategy;
}
public void contextInterface()
{
strategy.algorithmInterface();
}
}
//客戶端程式碼
public class Main
{
public static void main(String[] args)
{
Context context;
context = new Context(new ConcreteStrategyA());
context.contextInterface();
context = new Context(new ConcreteStrategyB());
context.contextInterface();
context = new Context(new ConcreteStrategyC());
context.contextInterface();
}
}
所以我們的可以進行以下修改:
//CashContext類
public class CashContext
{
CashSuper cashSuper;
public CashContext(CashSuper cashSuper)
{
this.cashSuper = cashSuper;
}
public double acceptCash(double money)
{
return cashSuper.acceptCash(money);
}
}
//客戶端程式碼
public class Main
{
private static double total = 0;
public static void main(String[] args)
{
consume("正常收費", 1, 1000);
consume("滿300返100", 1, 1000);
consume("打8折", 1, 1000);
System.out.println("總計:" + total);
}
public static void consume(String type, int num, double price)
{
CashContext cashContext = null;
if ("正常收費".equals(type))
{
cashContext = new CashContext(new CashNormal());
}
else if ("滿300返100".equals(type))
{
cashContext = new CashContext(new CashReturn(300, 100));
}
else if ("打8折".equals(type))
{
cashContext = new CashContext(new CashRebate(0.8));
}
double totalPrices = cashContext.acceptCash(num * price);
total += totalPrices;
System.out.println("單價:" + price + " 數量:" + num + "合計:" + totalPrices);
}
}
問題:缺乏工廠模式的優勢,在客戶端要進行判斷。
版本5
將策略模式與簡單工廠模式結合起來:
//改造後的CashContext
public class CashContext
{
CashSuper cashSuper;
public CashContext(CashSuper cashSuper)
{
this.cashSuper = cashSuper;
}
public CashContext(String type)
{
if ("正常收費".equals(type))
{
cashSuper = new CashNormal();
}
else if ("滿300返100".equals(type))
{
cashSuper = new CashReturn(300, 100);
}
else if ("打8折".equals(type))
{
cashSuper = new CashRebate(0.8);
}
}
public double acceptCash(double money)
{
return cashSuper.acceptCash(money);
}
}
//客戶端程式碼
public class Main
{
private static double total = 0;
public static void main(String[] args)
{
consume("正常收費", 1, 1000);
consume("滿300返100", 1, 1000);
consume("打8折", 1, 1000);
System.out.println("總計:" + total);
}
public static void consume(String type, int num, double price)
{
CashContext cashContext = new CashContext(type);
double totalPrices = cashContext.acceptCash(num * price);
total += totalPrices;
System.out.println("單價:" + price + " 數量:" + num + "合計:" + totalPrices);
}
}
簡單工廠 和 策略模式+簡單工廠 的對比
客戶端程式碼:
//簡單工廠模式的用法
CashSuper csuper = CashFactory.createCash(type);
...
totalPrices = csuper.acceptCash(num * price);
//策略模式與簡單工廠模式結合的用法
CashContext cashContext = new CashContext(type);
double totalPrices = cashContext.acceptCash(num * price);
簡單工廠模式需要讓客戶端認識兩個類,CashSuper和CashFactory,而策略模式與簡單工廠模式結合的用法,客戶端就只需要認識一個類CashContext。耦合度更加降低。
我們在客戶端例項化的是CashContext的物件,呼叫的是CashContext的方法,這使得具體的收費演算法徹底地與客戶端分離。連演算法的父類CashSuper都不讓客戶端認識了。相當於建立了一個控制代碼類。
總結
策略模式是一種定義一系列演算法的方法,從概念上來看,所有這些演算法完成的都是相同的工作,只是實現不同,它可以以相同的方式呼叫所有的演算法,減少了各種演算法與使用演算法之間的耦合。
另外,它簡化了單元測試,因為每個演算法都有自己的類,可以通過自己的介面單獨測試。
遺留問題:如果我們需要增加一種演算法,比如滿200返50,你就必須要改CashContext中的if或switch程式碼,有沒有更低的維護成本?
(使用反射)