1. 程式人生 > >【設計模式】策略模式——以商場促銷為例

【設計模式】策略模式——以商場促銷為例

本文內容參考自《大話設計模式》(程傑 著)

注:以下程式碼為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程式碼,有沒有更低的維護成本?
(使用反射)