商場促銷-策略模式-讀《大話設計模式》筆記
?讀《大話設計模式》第2章,閱讀筆記和感悟,建議購買原書閱讀,文中作者即原書《大話設計模式》一書作者。
1 商場收銀軟件
本章對應一個題目“做一個商場收銀軟件,營業(yè)員根據(jù)客戶所購買商品的單價和數(shù)量,向客戶收費?!?/p>
界面可設計如下:
商場收銀系統(tǒng)界面界面采用WPF編寫,界面相關代碼就不貼了,下面是點擊確定計算代碼:
//?計算總計
double?total?=?0.0d;
private?void?btnOK_Click(object?sender,?RoutedEventArgs?e)
{
??double?totalPrices?=?Convert.ToDouble(txtPrice.Text)?*?Convert.ToDouble(txtNum.Text);?//?計算每個商品的合計
??total?=?total?+?totalPrices;???????????????//?將每個商品合計計入總計
??lbxList.Items.Add($"單價:{txtPrice.Text}?數(shù)量:{txtNum.Text}?合計:{totalPrices}");?//?在列表框中顯示信息
??lblResult.Text?=?total.ToString();
}
小菜:很簡單啊,不到半小時就可以搞定。
大鳥:現(xiàn)在要求商場對商品搞活動,所有商品打八折。
小菜:那不就是在totalPrice后面乘以一個0.8嗎?
大鳥:問題是:難道商場活動結束,不打折了,還要再改一遍程序代碼,然后再用改后的程序去把所有機器全部安裝一次嗎?再說,還有可能因為周年慶,打五折的情況,是你,你怎么辦?
小菜:我想得是簡單了點,其實只要加個下拉框就可以解決你說的問題。
大鳥微笑不語。
2 增加打折
商場收銀系統(tǒng)v1.1關鍵代碼如下:
double?total?=?0.0d;
public?MainWindow()
{
??InitializeComponent();
??cbxType.Items.Add("正常收費");
??cbxType.Items.Add("打八折");
??cbxType.Items.Add("打七折");
??cbxType.Items.Add("打五折");
??cbxType.SelectedIndex?=?0;
}
private?void?btnOK_Click(object?sender,?RoutedEventArgs?e)
{
??double?totalPrices?=?0d;
??//?根據(jù)選項決定打折額度
??switch?(cbxType.SelectedIndex)
??{
????case?0:
??????totalPrices?=?Convert.ToDouble(txtPrice.Text)?*?Convert.ToDouble(txtNum.Text);
??????break;
????case?1:
??????totalPrices?=?Convert.ToDouble(txtPrice.Text)?*?Convert.ToDouble(txtNum.Text)?*?0.8;
??????break;
????case?2:
??????totalPrices?=?Convert.ToDouble(txtPrice.Text)?*?Convert.ToDouble(txtNum.Text)?*?0.7;
??????break;
????case?3:
??????totalPrices?=?Convert.ToDouble(txtPrice.Text)?*?Convert.ToDouble(txtNum.Text)?*?0.5;
??????break;
??}
??total?=?total?+?totalPrices;
??lbxList.Items.Add($"單價:{txtPrice.Text}?數(shù)量:{txtNum.Text}?{cbxType.SelectedItem}?合計:{totalPrices}");
??lblResult.Text?=?total.ToString();
}
小菜:"這下可以了吧,只要我事先把商場可能的打折做成下拉選擇框的項,要變化的可能性就小多了。"
加入折扣“這比剛才靈活性上是好多了,不過重復代碼很多,像Convert.ToDouble(),你這里就寫了8遍,而且4個分支要執(zhí)行的語句除了打折多少外幾乎沒什么不同,應該考慮重構一下。不過這不是最主要的,現(xiàn)在我的需求又來了,商場的活動加大,需要有滿300返100的促銷算法,你說怎么辦?”
“滿300返200,那要是700就要返200了?這個必須要寫函數(shù)了吧?”
“小菜呀,看來之前教你的白教了,這里看不出什么名堂嗎?”
“哦!我想起來了,你的意思是簡單工廠模式,是吧?對的對的,我可以先寫一個父類,再繼承它實現(xiàn)多個打折和反利的子類,利用多態(tài),完成這個代碼?!?/p>
“你打算寫幾個子類?”
“根據(jù)需求呀,比如八折、七折、五折、滿300送100、滿200送50......要幾個寫幾個。”
“小菜又不動腦子了,有必要這樣嗎?如果我現(xiàn)在要三折,我要滿300送80,你難道再去加子類?你不想想看,這當中哪些是相同的,哪些是不同的?”
3 簡單工廠實現(xiàn)
“對的,這里打折基本都是一樣的,只要有個初始化參數(shù)就可以了。滿幾送幾的,需要兩個參數(shù)才行,明白,現(xiàn)在看來不麻煩了?!?/p>
“面向對象的編程,并不是類越多越好,類的劃分是為了封裝,但分類的基礎是抽象,具有相同屬性和功能的對象的抽象集合才是類。打一折和九折只是形式的不同,抽象分析出來,所有的打折算法都是一樣的,所以打折算法應該是一個類。好了,空話已說了太多,寫出來才是真的懂?!?/p>
代碼結構圖
代碼結構圖現(xiàn)金收費抽象類
///?
///?現(xiàn)金收費抽象類
///?
abstract?class?CashSuper
{
??///?
??///?收取現(xiàn)金,參數(shù)為原價,返回為當前價
??///?
??///?
??///?
??public?abstract?double?AcceptCash(double?money);
}
正常收費子類
///?
///?正常收費子類
///?
class?CashNormal?:?CashSuper
{
??///?
??///?正常收費,原價返回
??///?
??///?
??///?
??public?override?double?AcceptCash(double?money)
??{
????return?money;
??}
}
打折收費子類
///?
///?打折收費子類
///?
class?CashRebate?:?CashSuper
{
??private?double?moneyRebate?=?1d;
??///?
??///?打折收費,初始化時,必需輸入折扣率,如八折,就是0.8
??///?
??///?
??public?CashRebate(string?moneyRebate)
??{
????this.moneyRebate?=?double.Parse(moneyRebate);
??}
??public?override?double?AcceptCash(double?money)
??{
????return?money?*?moneyRebate;
??}
}
返利收費子類
///?
///?返利收費子類
///?
class?CashReturn?:?CashSuper
{
??private?double?moneyCondition?=?0.0d;
??private?double?moneyReturn?=?0.0d;
??///?
??///?返利收費,初始化時必須要輸入返利條件和返利值,比如滿300返100,則moneyCondition為300,moneyReturn為100
??///?
??///?
??///?
??public?CashReturn(string?moneyCondition,?string?moneyReturn)
??{
????this.moneyCondition?=?double.Parse(moneyCondition);
????this.moneyReturn?=?double.Parse(moneyReturn);
??}
??public?override?double?AcceptCash(double?money)
??{
????double?result?=?money;
????//?若大于返利條件,則需要減去返利值
????if?(money?>=?moneyCondition)
????{
??????result?=?money?-?Math.Floor(money?/?moneyCondition)?*?moneyReturn;
????}
????return?result;
??}
}
現(xiàn)金收費工廠類
///?
///?現(xiàn)金收費工廠類
///?
class?CashFactory
{
??public?static?CashSuper?CreateCashAccept(string?type)
??{
????CashSuper?cs?=?null;
????switch(type)
????{
??????case?"正常收費":
????????cs?=?new?CashNormal();
????????break;
??????case?"滿300返100":
????????CashReturn?cr1?=?new?CashReturn("300",?"100");
????????cs?=?cr1;
????????break;
??????case?"打八折":
????????CashRebate?cr2?=?new?CashRebate("0.8");
????????cs?=?cr2;
????????break;
????}
????return?cs;
??}
}
客戶端程序主要部分
double?total?=?0.0d;
private?void?btnOK_Click(object?sender,?RoutedEventArgs?e)
{
??//?利用簡單工廠模式根據(jù)下拉選擇框,生成相應的對象
??CashSuper?csuper?=?CashFactory.CreateCashAccept(cbxType.SelectedItem.ToString());
??double?totalPrices?=?0d;
??//?通過多態(tài),可以得到收取費用的結果
??totalPrices?=?csuper.AcceptCash(Convert.ToDouble(txtPrice.Text)*Convert.ToDouble(txtNum.Text));
??total?=?total?+?totalPrices;
??lbxList.Items.Add($"單價:{txtPrice.Text}?數(shù)量:{txtNum.Text}?{cbxType.SelectedItem}?合計:{totalPrices}");
??lblResult.Text?=?total.ToString();
}
“大鳥,搞定,這次無論你要怎么改,我都可以簡單處理就行了?!毙〔俗孕艥M滿地說。
“是嗎?我要是需要打五折和滿500送200的促銷活動,如何辦?”
“只要在現(xiàn)金工廠當中加兩個條件,在界面的下拉框里加兩項,就OK了?!?/p>
“現(xiàn)金工廠?!你當量生成鈔票呀。是收費對象生成工廠才準確。說得不錯,如果我現(xiàn)在需要增加一種商場促銷手段,滿100積分10點,以后積分到一定時候可以領取獎品如何做?”
“有了工廠,何難?加一個積分算法,構造方法有兩個參數(shù):條件和返點,讓它繼承CashSuper,再到現(xiàn)金工廠,哦,不對,是收費對象生成工廠里增加滿100積分10點的分支條件,再到界面稍加改動,就行了?!?/p>
“嗯,不錯。你對簡單工廠用得很熟練了嘛?!贝篪B接著說:“簡單工廠模式雖然也能解決這個問題,但這個模式只是解決對象的創(chuàng)建問題,而且由于工廠本身包括了所有的收費方式,商場是可能經(jīng)常性地更改打折額度和返利額度,每次維護或擴展收費方式都要改動這個工廠,以致代碼需要重新編譯部署,這真的是很糟糕的處理方式,所有用它不是最好的辦法。面對算法的進學變動,應該有更好的辦法。好好去研究一下其他的設計械,你會找到答案的?!?/p>
小菜進入了沉思中......
4 策略模式
小菜次日來找大鳥,說:“我找到相關的設計模式了,應該是策略模式(Strategy)。策略模式定義了算法家族,分別封裝起來,讓它們之間可以相互替換,此模式讓算法的變化,不會影響到使用算法的客戶。看來商場收銀系統(tǒng)應該考慮用策略模式?”
"你問我?你說呢?"大鳥笑道,“商場收銀時如何促銷,用打折還是返利,其實都是一些算法,用工廠來生成算法對象,這沒有錯,但算法本身只是一種策略,最重要的是這些算法是隨時都可能互相替換的,這就是變化點,而封裝變化點是我們面向對象的一種很重要的思維方式。我們來看看策略模式的結構圖和基本代碼?!?/p>
策略模式(Strategy)結構圖
Strategy類,定義所有支持的算法的公共接口
//?抽象算法類
abstract?class?Strategy
{
??//?算法方法
??public?abstract?void?AlgorithmInterface();
}
ConcreteStrategy,封裝了具體的算法或行為,繼承于Strategy
//?具體算法A
class?ConcreteStrategyA:Strategy
{
??//?算法A實現(xiàn)方法
??public?override?void?AlgorithmInterface()
??{
????Console.WriteLine("算法A實現(xiàn)");
??}
}
//?具體算法B
class?ConcreteStrategyB?:?Strategy
{
??//?算法B實現(xiàn)方法
??public?override?void?AlgorithmInterface()
??{
????Console.WriteLine("算法B實現(xiàn)");
??}
}
//?具體算法C
class?ConcreteStrategyC?:?Strategy
{
??//?算法C實現(xiàn)方法
??public?override?void?AlgorithmInterface()
??{
????Console.WriteLine("算法C實現(xiàn)");
??}
}
Context,用一個ConcreteStratege來配置,維護一個對Strategy對象的引用。
//?上下文
class?Context
{
??Strategy?strategy;
??//?初始化時,傳入具體的策略對象
??public?Context(Strategy?strategy)
??{
????this.strategy?=?strategy;
??}
??//?上下文接口,根據(jù)具體的策略對象,調用其算法的方法
??public?void?ContextInterface()
??{
????strategy.AlgorithmInterface();
??}
}
客戶端代碼
static?void?Main(string[]?args)
{
??Context?context;
??//?由于實例化不同的策略,所以最終在調用context.ContextInterface();時,所獲得的結果就不盡相同
??context?=?new?Context(new?ConcreteStrategyA());
??context.ContextInterface();
??context?=?new?Context(new?ConcreteStrategyB());
??context.ContextInterface();
??context?=?new?Context(new?ConcreteStrategyC());
??context.ContextInterface();
}
5 策略模式實現(xiàn)
“我明白了,”小菜說,“我昨天寫的CashSuper就是抽象策略,而正常收費CashNormal、打折收費CashRebate和返利收費CashRetrun就是三個具體策略,也就是策略模式中說的具體算法,對吧?”
“是的,來吧,你模仿策略模式的基本代碼,改寫一下你的程序?!?/p>
“其實不麻煩,原來寫的CashSuper、CashNormal、CashRebate和CashReturn都不用更改了,只要加一個CashContext類,并改寫一下客戶端就行了?!?/p>
商場收銀系統(tǒng)v1.2 代碼結構圖
商場收銀系統(tǒng)v1.2 代碼結構圖CashContext類
class?CashContext
{
??private?CashSuper?cs;
??//?通過構造方法,傳入具體的收費策略
??public?CashContext(CashSuper?csuper)
??{
????this.cs?=?csuper;
??}
??//?根據(jù)收費策略的不同,獲得計算結果
??public?double?GetResult(double?money)
??{
????return?cs.AcceptCash(money);
??}
}
客戶端主要代碼
//?用于總計
double?total?=?0.0d;
private?void?btnOK_Click(object?sender,?RoutedEventArgs?e)
{
??CashContext?cc?=?null;
??//?根據(jù)下拉選擇框,將相應的策略對象作為參數(shù)傳入CashContext的對象中
??switch?(cbxType.SelectedItem.ToString())
??{
????case?"正常收費":
??????cc?=?new?CashContext(new?CashNormal());
??????break;
????case?"滿300返100":
??????cc?=?new?CashContext(new?CashReturn("300",?"100"));
??????break;
????case?"打8折":
??????cc?=?new?CashContext(new?CashRebate("0.8"));
??????break;
??}
??double?totalPrices?=?0d;
??//?通過對Context的GetResult方法的調用,可以得到收取費用的結果,讓具體算法與客戶進行了隔離
??totalPrices?=?cc.GetResult(Convert.ToDouble(txtPrice.Text)?*?Convert.ToDouble(txtNum.Text));
??total?=?total?+?totalPrices;
??lbxList.Items.Add($"單價:{txtPrice.Text}?數(shù)量:{txtNum.Text}?{cbxType.SelectedItem}?合計:{totalPrices}");
??lblResult.Text?=?total.ToString();
}
“大鳥,代碼是模仿著寫出來了。但我感覺這樣子做不又回到了原來的老路了嗎?在客戶端去判斷用哪一個算法?”
“是的,但是你有沒有什么好辦法,把這個判斷的過程從客戶端程序轉移走呢?”
“轉移?不明白,原來我用簡單工廠是可以轉移的,現(xiàn)在這樣子如何做到?”
“難道簡單工廠就一定要是一個單獨的類嗎?難道不可以與策略模式的Context結合?”
“哦,我明白你的意思了。我試試看。”
6 策略與簡單工廠結合
改造后的CashContext
class?CashContext
{
??CashSuper?cs?=?null;
??//?注意參數(shù)不是具體的收費策略對象,而是一個字符串,表示收費類型
??//?將實例化具體策略的過程由客戶端轉移到Context類中。簡單工廠的應用
??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)
??{
????return?cs.AcceptCash(money);
??}
}
客戶端窗體程序的主要部分代碼
double?total?=?0.0d;
private?void?btnOK_Click(object?sender,?RoutedEventArgs?e)
{
??//?根據(jù)下拉選擇框,將相應的算法類型字符串傳入CashContext的對象中
??CashContext?cc?=?new?CashContext(cbxType.SelectedItem.ToString());
??double?totalPrices?=?0d;
??totalPrices?=?cc.GetResult(Convert.ToDouble(txtPrice.Text)?*?Convert.ToDouble(txtNum.Text));
??total?=?total?+?totalPrices;
??lbxList.Items.Add($"單價:{txtPrice.Text}?數(shù)量:{txtNum.Text}?{cbxType.SelectedItem}?合計:{totalPrices}");
??lblResult.Text?=?total.ToString();
}
“嗯,原來簡單工廠模式并非只有建一個工廠類的做法,還可以這樣子做。此時比剛才的模仿策略模式的寫法要清楚多了,客戶端代碼簡單明了?!?/p>
“那和你寫的簡單工廠的客戶端代碼比呢?觀察一下,找出它們的不同之處。”
//?簡單工廠模式的用法
CashSuper?csuper?=?CashFactory.CreateCashAccept(cbxType.SelectedItem.ToString());
...?=?csuper.GetResult(...);
//?策略模式與簡單工廠結合的用法
CashContext?csuper?=?new?CashContext(cbxType.SelectedItem.ToString());
...?=?csuper.GetSult(...);
"你的意思是說,簡單工廠模式我需要讓客戶端認識兩個類,CashSuper和CashFactory,而策略模式與簡單工廠結合的用法,客戶端就只需要認識一個類CashContext就可以了。耦合更加降低。"
“說的沒錯,我們在客戶端實例化的是CashContext的對象,調用的是CashContext的方法GetResult,這使得具體的收費算法徹底地與客戶端分離。連算法的父類CashSuper都不讓客戶端認識了?!?/p>
7 策略模式解析
“回過頭來反思一下策略模式,策略模式是一種定義一系列算法的方法,從概念上來看,所有這些算法完成的都是相同的工作,只是實現(xiàn)不同,它可以以相同的方式調用所有的算法,減少了各種算法類與使用算法類之間的耦合。”大鳥總結道。
“策略模式還有什么優(yōu)點?”小菜問道。
“策略模式的Statege類層次為Context定義了一系列的可供重用的算法或行為。繼承有助于析取出這些算法中的公共功能。對于打折、返利或者其他的算法,其實都是對實際商品收費的一種計算方式,通過繼承,可以得到它們的公共功能,你說這公共功能指什么?”
"公共的功能就是獲得計算費用的結果GetResult,這使得算法間有了抽象的父類CashSuper。"
“對,很好。別外一個策略模式的優(yōu)點是簡化了單元測試,因為每個算法都有自己的類,可以通過自己的接口單獨測試。”
“每個算法可保證它沒有錯誤,修改其中任一個時也不會影響其他的算法。這真的是非常好。”
“哈,小菜今天表現(xiàn)不錯,我所想的你都想到了?!贝篪B表揚了小菜,“還有,在最開始編程時,你不得不在客戶端的代碼中為了判斷用哪一個算法計算而用了switch條件分支,這也是正常的。因為,當不同的行為堆砌在一個類中時,就很難避免使用條件語句來選擇合適的行為。將這些行為封裝在一個個獨立的Strategy類中,可以在使用這些行為的類中消除條件語句。就商場收銀系統(tǒng)的例子而言,在客戶端的代碼中就消除條件語句,避免了大量的判斷。這是非常重要的進展。你能用一句話來概況這個優(yōu)點嗎?”大鳥總結后問道。
“策略模式封閉了變化?!毙〔丝焖俣鴪远ǖ恼f。
“說得非常好,策略模式就是用來封裝算法的,但在實踐中,我們發(fā)現(xiàn)可以用它來封裝幾乎任何類型的規(guī)則,只要在分析過程中聽到需要在不同時間應用不同的業(yè)務規(guī)則,就可以考慮使用策略模式處理這種變化的可能性。”
“但我感覺在基本的策略模式中,選擇所用具體實現(xiàn)的職責由客戶端對象承擔,并轉給策略模式的Context對象。這本身并沒有解除客戶端需要選擇判斷的壓力,而策略模式與簡單工廠模式結合后,選擇具體實現(xiàn)的職責也可以由Context來承擔,這就最大化地減輕了客戶端職責?!?/p>
“是的,這已經(jīng)比起初的策略模式好用了,不過,它依然不夠完美。”
“哦,還有什么不足嗎?”
“因為在CashContext里還是用到了switch,也就是說,如果我們需要增加一種算法,比如'滿200送50',你就必須要更改CashContext中的switch代碼,這總還是讓人很不爽呀?!?/p>
“那你說怎么辦,有需求就得改呀,任何需求的變更都是需要成本的。”
“但是成本的高低還是有差異的。高手和菜鳥的區(qū)別就是高手可以花同樣的代碼獲得最大的收益或者說做同樣的事花最小的代價。面對同樣的需求,當然是改動越小越好。”
“你的意思是說,還有更好的辦法?”
“當然。這個辦法就是用到了反射技術,不是常有人講,'反射反射,程序員的快樂',不過今天就不講了,以后會再提它的?!?/p>
“反射真有這么神奇?”小菜疑惑地望向了遠方。
(注:抽象工廠模式章節(jié)有對反射的講解)
本文幾乎照抄原書章節(jié),看書加手打文字,加深知識點理解,建議閱讀原書效果更佳。
本文轉自微信公眾號:樂趣課堂
原文鏈接:https://mp.weixin.qq.com/s/pIR3gmc6HfqKQjuRrFsfGQ
