前情提要
上集講到, 小光引入了飲料機(工廠方法模式)改進了光氏飲品的生產過程. 現在如果要新上什麼飲品, 改變配方什麼的, 都很簡單了, 直接增加一個飲料機, 或是替換/拿掉一個飲料機就可以了. 表妹再也不抱怨了.
小光也找了些飲料廠商拿到了一些試喝的飲料新品. 心想, 正好臨近感恩節, 聖誕節, 雙十二啥的, 我可以拿這些飲料新品來做些活動啊, 感恩下新老顧客啊… 這些新品小光可是自己親身試喝過的, 絕對好喝, 小光不做奸商, ?
所有示例原始碼已經上傳到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)無需改變, 也無需關注每個演算法的具體實現.
擴充套件閱讀一
實際上策略模式也還是利用抽象, 封裝, 繼承, 多型的物件導向特性, 來達到封裝變化, 解耦合的. 典型的開閉原則的實踐.
另外, 眼尖的同學可能看到, 貌似這個類圖似曾相識啊. 前面講的簡單工廠和工廠方法中的類圖與此極其相似:
比較上圖三個模式的紅框部分, 我們可以發現, 相當一致. 在此明確下三者的關係與區別:
- 首先簡單工廠和工廠方法是建立型的模式, 而策略模式是行為型的模式.
- 所謂建立型就是說用來生產物件的, 注重的生產(new)這個部分, 用建立型的模式來代替直接new一個例項, 更多是想將直接的例項依賴通過不同的方法轉化介面依賴.
- 所謂行為型模式更多是描述一種行為, 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為例, 其圖片的磁碟快取就使用了策略模式, 並提供了很多策略供使用者選擇:
優秀原始碼總是設計巧妙但又易懂不晦澀.
活動搞起了, 小光熱乾麵歡迎大家常來光臨啊, 喜歡您就收藏, 喜歡您就關注…