回饋顧客, 活動搞起 — 策略模式

anly_jun發表於2019-03-01

前情提要

上集講到, 小光引入了飲料機(工廠方法模式)改進了光氏飲品的生產過程. 現在如果要新上什麼飲品, 改變配方什麼的, 都很簡單了, 直接增加一個飲料機, 或是替換/拿掉一個飲料機就可以了. 表妹再也不抱怨了.

小光也找了些飲料廠商拿到了一些試喝的飲料新品. 心想, 正好臨近感恩節, 聖誕節, 雙十二啥的, 我可以拿這些飲料新品來做些活動啊, 感恩下新老顧客啊… 這些新品小光可是自己親身試喝過的, 絕對好喝, 小光不做奸商, ?

所有示例原始碼已經上傳到Github, 戳這裡

活動策劃

小光以其獨特的碼農生意人思維(我也不知道這是什麼…), 很快想出了幾條活動方案:

  • 即日起, 到感恩節(11/24)那天, 所有飲品6折優惠.
  • 雙十二當天, 滿12立減2元.
  • 12月20號到聖誕節(12/25), 買熱乾麵+飲料套餐送大蘋果.

小光想出這些活動方案後, 屁顛屁顛去拿給表妹看. 沒曾想, 表妹一臉不愉快, 這麼多方案, 還這麼負責, 我怎麼記得住…(單細胞的表妹).

怎麼辦了, 小光可不想放棄自己好不容易想出的這些方案, 而且活動方案肯定會因為是不同節日而有所改變嘛.

解決之道

很快, 小光就想到了解決辦法, 他將三種活動方案的演算法做好, 內建在收銀臺. 在不同的日子裡選用不用的演算法策略.

“我真是個天才, 哈哈哈哈”, 小光想著都快笑出聲了…

照例, 收銀員無需關注是什麼具體的演算法, 故而抽象出一個父級介面:

public interface ActivityStrategy {

    String getActivityPrice();
}複製程式碼

每個方案對應一個演算法策略:

// 感恩節活動演算法
public class ThanksGivingDayStrategy implements ActivityStrategy {

    @Override
    public String getActivityPrice() {
        // 經過一系列演算法
        return "(感恩節)所有飲品一律5折";
    }
}

// 雙十二演算法
public class DoubleTwelveDayStrategy implements ActivityStrategy {

    @Override
    public String getActivityPrice() {
        // 經過一系列演算法
        return "(雙十二)滿12立減2元";
    }
}

// 聖誕節演算法
public class ChristmasStrategy implements ActivityStrategy {

    @Override
    public String getActivityPrice() {
        // 經過一系列演算法
        return "(聖誕節)買熱乾麵+飲品套餐, 送大蘋果一個";
    }
}

// 預設演算法(注意這個, 稍後的擴充套件閱讀會說下這個Default實現的意義)
public class DefaultActivityStrategy implements ActivityStrategy {
    @Override
    public String getActivityPrice() {
        // 什麼都不做
        return "沒有活動";
    }
}複製程式碼

支援各種活動策略演算法的收銀臺:

// 收銀臺
public class Checkstand {

    private ActivityStrategy mActivityStrategy;

    public Checkstand() {
        mActivityStrategy = new DefaultActivityStrategy();
    }

    public Checkstand(ActivityStrategy activityStrategy) {
        this.mActivityStrategy = activityStrategy;
    }

    public void setActivityStrategy(ActivityStrategy activityStrategy) {
        this.mActivityStrategy = activityStrategy;
    }

    public void printBill() {
        System.out.println("本次賬單活動:" + mActivityStrategy.getActivityPrice());
    }
}複製程式碼

投入使用

活動方案演算法和收銀臺完工之後, 小光立馬投入了使用:

public class XiaoGuang {

    public static void main(String[] args) {

        // 收銀臺, 預設
        Checkstand checkstand = new Checkstand();
        checkstand.printBill();

        // 感恩節期間
        checkstand.setActivityStrategy(new ThanksGivingDayStrategy());
        checkstand.printBill();

        // 雙十二
        checkstand.setActivityStrategy(new DoubleTwelveDayStrategy());
        checkstand.printBill();

        // 聖誕節期間
        checkstand.setActivityStrategy(new ChristmasStrategy());
        checkstand.printBill();
    }
}複製程式碼

結果, 也正如小光預料的:

本次賬單活動:沒有活動
本次賬單活動:(感恩節)所有飲品一律5折
本次賬單活動:(雙十二)滿12立減2元
本次賬單活動:(聖誕節)買熱乾麵+飲品套餐, 送大蘋果一個複製程式碼

活動一經推出, 顧客果然是比以前更多了…
大家還對小光新推出的那些試喝飲料讚不絕口, 都覺得味道不錯, 還很著很有意思的名字…

故事之後

照例, 故事之後, 我們用UML類圖來梳理下上述的關係(關注收銀臺與活動策略演算法之間的關係):

回饋顧客, 活動搞起 — 策略模式

大家可能已經看出端倪了, 沒錯, 這就是策略模式.

策略模式(Strategy Pattern):
定義一組演算法, 並將每一個單獨演算法封裝起來, 讓它們可以相互替換.

策略模式讓演算法獨立於使用它的客戶而變化, 例如如果明年小光的雙十二活動改變了, 只需單獨修改這個DoubleTwelveDayStrategy即可, 客戶類(收銀臺Checkstand)無需改變, 也無需關注每個演算法的具體實現.

擴充套件閱讀一

實際上策略模式也還是利用抽象, 封裝, 繼承, 多型的物件導向特性, 來達到封裝變化, 解耦合的. 典型的開閉原則的實踐.

另外, 眼尖的同學可能看到, 貌似這個類圖似曾相識啊. 前面講的簡單工廠工廠方法中的類圖與此極其相似:

回饋顧客, 活動搞起 — 策略模式

比較上圖三個模式的紅框部分, 我們可以發現, 相當一致. 在此明確下三者的關係與區別:

  1. 首先簡單工廠工廠方法建立型的模式, 而策略模式行為型的模式.
  2. 所謂建立型就是說用來生產物件的, 注重的生產(new)這個部分, 用建立型的模式來代替直接new一個例項, 更多是想將直接的例項依賴通過不同的方法轉化介面依賴.
  3. 所謂行為型模式更多是描述一種行為, A使用B, 怎麼使用的這個關係上.

實際上, 在上個工廠方法的故事中, 我們就已經使用到了策略模式.

表妹選擇不同的飲料機來那飲料, 這個行為實際上就是一個策略模式的體現, 回顧下表妹的程式碼:

public class Cousins {

    private IBeverageMachine mBeverageMachine;

    private void setBeverageMachine(IBeverageMachine machine) {
        this.mBeverageMachine = machine;
    }

    private Drink takeDrink() {
        if (mBeverageMachine == null) throw new NullPointerException("Should set Beverage Machine firstly.");

        return mBeverageMachine.makeDrink();
    }

    public static void main(String[] args) {

        Cousins cousins = new Cousins();

        // for A
        cousins.setBeverageMachine(new OrangeJuiceMachine());
        Drink drink = cousins.takeDrink();
        System.out.println(drink);

        // for B
        cousins.setBeverageMachine(new CokeMachine());
        System.out.println(cousins.takeDrink());

        // for D
        cousins.setBeverageMachine(new MilkTeaMachine());
        System.out.println(cousins.takeDrink());
    }
}複製程式碼

和我們這個收銀臺(Checkstand)是一樣一樣的, 上例中的模式使用實際可以理解成是這樣:

回饋顧客, 活動搞起 — 策略模式

  • 藍色部分是工廠方法模式的使用, 作用在於生產出不同的飲品.
  • 紅色部分是策略模式的使用, 作用在於讓表妹根據實際情況選擇不同的飲料機.

所以說模式的運用, 往往不是簡單而單一, 很多時候是很多模式合在一起的.

擴充套件閱讀二

在展示本例策略模式的UML類圖時, 我們將DefaultActivityStrategy類標記成紅色了, 這是為什麼呢?

是因為這裡我們用的這個DefaultActivityStrategy實際上也是一種設計模式的體現. 這個模式不在GoF的23中設計模式內, 但是絕對是一個很常用, 很實用的模式 — 空物件模式.

空物件模式(Null Object Pattern):
用一個空(什麼都不做的)物件來代替NULL.

空物件模式是一個很簡單的設計模式, 也可以看成是一種編碼習慣. 它小但是作用大:

  • 使用空物件模式可以減少很多我們對於物件是否為空的判斷. 例如本例中, 如果Checkstand的無參建構函式我們沒有new一個空物件, 那麼後續的對於Checkstand例項各種呼叫我們可能就需要判斷其mActivityStrategy是否為空. 如果遺漏, 很有可能導致null pointer異常.
  • 另外對於一些可以鏈式呼叫的物件, 如果我們要每次都判斷是否為空會很影響我們的鏈式呼叫.

空物件模式經常會用來作為策略模式演算法族中的一個, 來提供空策略.

擴充套件閱讀三

策略模式由於其優秀的對外擴充套件性和對內封裝性, 在一些SDK或是優秀開源庫中會經常用到. 還是以Glide為例, 其圖片的磁碟快取就使用了策略模式, 並提供了很多策略供使用者選擇:

回饋顧客, 活動搞起 — 策略模式

優秀原始碼總是設計巧妙但又易懂不晦澀.

活動搞起了, 小光熱乾麵歡迎大家常來光臨啊, 喜歡您就收藏, 喜歡您就關注…

相關文章