1 場景問題#
1.1 報價管理##
向客戶報價翅敌,對于銷售部門的人來講芙委,這是一個非常重大逞敷、非常復雜的問題,對不同的客戶要報不同的價格灌侣,比如:
- 對普通客戶或者是新客戶報的是全價
- 對老客戶報的價格推捐,根據客戶年限,給予一定的折扣
- 對大客戶報的價格侧啼,根據大客戶的累計消費金額牛柒,給予一定的折扣
還要考慮客戶購買的數量和金額堪簿,比如:雖然是新用戶,但是一次購買的數量非常大皮壁,或者是總金額非常高椭更,也會有一定的折扣
還有萤彩,報價人員的職務高低国拇,也決定了他是否有權限對價格進行一定的浮動折扣
甚至在不同的階段民褂,對客戶的報價也不同锥惋,一般情況是剛開始比較高勿负,越接近成交階段眶痰,報價越趨于合理厕诡。
總之额湘,向客戶報價是非常復雜的描馅,因此在一些CRM(客戶關系管理)的系統(tǒng)中把夸,會有一個單獨的報價管理模塊,來處理復雜的報價功能铭污。
為了演示的簡潔性恋日,假定現在需要實現一個簡化的報價管理,實現如下的功能:
(1)對普通客戶或者是新客戶報全價
(2)對老客戶報的價格嘹狞,統(tǒng)一折扣5%
(3)對大客戶報的價格岂膳,統(tǒng)一折扣10%
該怎么實現呢?
1.2 不用模式的解決方案##
要實現對不同的人員報不同的價格的功能磅网,無外乎就是判斷起來麻煩點谈截,也不多難,很快就有朋友能寫出如下的實現代碼涧偷,示例代碼如下:
/**
* 價格管理簸喂,主要完成計算向客戶所報價格的功能
*/
public class Price {
/**
* 報價,對不同類型的燎潮,計算不同的價格
* @param goodsPrice 商品銷售原價
* @param customerType 客戶類型
* @return 計算出來的喻鳄,應該給客戶報的價格
*/
public double quote(double goodsPrice,String customerType){
if(customerType.equals("普通客戶 ")){
System.out.println("對于新客戶或者是普通客戶,沒有折扣 ");
return goodsPrice;
}else if(customerType.equals("老客戶 ")){
System.out.println("對于老客戶确封,統(tǒng)一折扣 5%");
return goodsPrice*(1-0.05);
}else if(customerType.equals("大客戶 ")){
System.out.println("對于大客戶除呵,統(tǒng)一折扣 10%");
return goodsPrice*(1-0.1);
}
// 其余人員都是報原價
return goodsPrice;
}
}
1.3 有何問題##
上面的寫法是很簡單的,也很容易想爪喘,但是仔細想想颜曾,這樣實現,問題可不小腥放,比如:第一個問題:價格類包含了所有計算報價的算法泛啸,使得價格類绿语,尤其是報價這個方法比較龐雜秃症,難以維護候址。
有朋友可能會想,這很簡單嘛种柑,把這些算法從報價方法里面拿出去岗仑,形成獨立的方法不就可以解決這個問題了嗎?據此寫出如下的實現代碼聚请,示例代碼如下:
/**
* 價格管理荠雕,主要完成計算向客戶所報價格的功能
*/
public class Price {
/**
* 報價,對不同類型的驶赏,計算不同的價格
* @param goodsPrice 商品銷售原價
* @param customerType 客戶類型
* @return 計算出來的炸卑,應該給客戶報的價格
*/
public double quote(double goodsPrice,String customerType){
if(customerType.equals("普通客戶 ")){
return this.calcPriceForNormal(goodsPrice);
}else if(customerType.equals("老客戶 ")){
return this.calcPriceForOld(goodsPrice);
}else if(customerType.equals("大客戶 ")){
return this.calcPriceForLarge(goodsPrice);
}
//其余人員都是報原價
return goodsPrice;
}
/**
* 為新客戶或者是普通客戶計算應報的價格
* @param goodsPrice 商品銷售原價
* @return 計算出來的,應該給客戶報的價格
*/
private double calcPriceForNormal(double goodsPrice){
System.out.println("對于新客戶或者是普通客戶煤傍,沒有折扣 ");
return goodsPrice;
}
/**
* 為老客戶計算應報的價格
* @param goodsPrice 商品銷售原價
* @return 計算出來的盖文,應該給客戶報的價格
*/
private double calcPriceForOld(double goodsPrice){
System.out.println("對于老客戶,統(tǒng)一折扣 5%");
return goodsPrice*(1-0.05);
}
/**
* 為大客戶計算應報的價格
* @param goodsPrice 商品銷售原價
* @return 計算出來的蚯姆,應該給客戶報的價格
*/
private double calcPriceForLarge(double goodsPrice){
System.out.println("對于大客戶五续,統(tǒng)一折扣 10%");
return goodsPrice*(1-0.1);
}
}
這樣看起來,比剛開始稍稍好點龄恋,計算報價的方法會稍稍簡單一點疙驾,這樣維護起來也稍好一些,某個算法發(fā)生了變化郭毕,直接修改相應的私有方法就可以了它碎。擴展起來也容易一點,比如要增加一個“戰(zhàn)略合作客戶”的類型显押,報價為直接8折链韭,就只需要在價格類里面新增加一個私有的方法來計算新的價格,然后在計算報價的方法里面新添一個else-if即可煮落〕ㄇ停看起來似乎很不錯了。
真的很不錯了嗎蝉仇?
再想想旋讹,問題還是存在,只不過從計算報價的方法挪動到價格類里面了轿衔,假如有100個或者更多這樣的計算方式沉迹,這會讓這個價格類非常龐大,難以維護害驹。而且鞭呕,維護和擴展都需要去修改已有的代碼,這是很不好的宛官,違反了開-閉原則葫松。
第二個問題:經常會有這樣的需要瓦糕,在不同的時候,要使用不同的計算方式腋么。
比如:在公司周年慶的時候咕娄,所有的客戶額外增加3%的折扣;在換季促銷的時候珊擂,普通客戶是額外增加折扣2%圣勒,老客戶是額外增加折扣3%,大客戶是額外增加折扣5%摧扇。這意味著計算報價的方式會經常被修改圣贸,或者被切換。
通常情況下應該是被切換扛稽,因為過了促銷時間旁趟,又還回到正常的價格體系上來了。而現在的價格類中計算報價的方法庇绽,是固定調用各種計算方式锡搜,這使得切換調用不同的計算方式很麻煩,每次都需要修改if-else里面的調用代碼瞧掺。
看到這里耕餐,可能有朋友會想, 那么到底應該如何實現辟狈,才能夠讓價格類中的計算報價的算法肠缔,能很容易的實現可維護、可擴展哼转,又能動態(tài)的切換變化呢明未?
2 解決方案#
2.1 策略模式來解決##
用來解決上述問題的一個合理的解決方案就是策略模式。那么什么是策略模式呢壹蔓?
- 策略模式定義
定義一系列的算法趟妥,把它們一個個封裝起來,并且使它們可相互替換佣蓉。本模式使得算法可獨立于使用它的客戶而變化披摄。
- 應用策略模式來解決的思路
仔細分析上面的問題,先來把它抽象一下勇凭,各種計算報價的計算方式就好比是具體的算法疚膊,而使用這些計算方式來計算報價的程序,就相當于是使用算法的客戶虾标。
再分析上面的實現方式寓盗,為什么會造成那些問題,根本原因,就在于算法和使用算法的客戶是耦合的傀蚌,甚至是密不可分的基显,在上面實現中,具體的算法和使用算法的客戶是同一個類里面的不同方法喳张。
現在要解決那些問題,按照策略模式的方式美澳,應該先把所有的計算方式獨立出來销部,每個計算方式做成一個單獨的算法類,從而形成一系列的算法制跟,并且為這一系列算法定義一個公共的接口舅桩,這些算法實現是同一接口的不同實現,地位是平等的雨膨,可以相互替換擂涛。這樣一來,要擴展新的算法就變成了增加一個新的算法實現類聊记,要維護某個算法撒妈,也只是修改某個具體的算法實現即可,不會對其它代碼造成影響排监。也就是說這樣就解決了可維護狰右、可擴展的問題。
為了實現讓算法能獨立于使用它的客戶舆床,策略模式引入了一個上下文的對象棋蚌,這個對象負責持有算法,但是不負責決定具體選用哪個算法挨队,把選擇算法的功能交給了客戶谷暮,由客戶選擇好具體的算法后,設置到上下文對象里面盛垦,讓上下文對象持有客戶選擇的算法湿弦,當客戶通知上下文對象執(zhí)行功能的時候,上下文對象會去轉調具體的算法腾夯。這樣一來省撑,具體的算法和直接使用算法的客戶是分離的。
具體的算法和使用它的客戶分離過后俯在,使得算法可獨立于使用它的客戶而變化竟秫,并且能夠動態(tài)的切換需要使用的算法,只要客戶端動態(tài)的選擇使用不同的算法跷乐,然后設置到上下文對象中去肥败,實際調用的時候,就可以調用到不同的算法。
2.2 模式結構和說明##
策略模式的結構示意圖如圖所示:
Strategy:策略接口馒稍,用來約束一系列具體的策略算法皿哨。Context使用這個接口來調用具體的策略實現定義的算法。
ConcreteStrategy:具體的策略實現纽谒,也就是具體的算法實現证膨。
Context:上下文,負責和具體的策略類交互鼓黔,通常上下文會持有一個真正的策略實現央勒,上下文還可以讓具體的策略類來獲取上下文的數據,甚至讓具體的策略類來回調上下文的方法澳化。
2.3 策略模式示例代碼##
- 首先來看策略崔步,也就是定義算法的接口,示例代碼如下:
/**
* 策略缎谷,定義算法的接口
*/
public interface Strategy {
/**
* 某個算法的接口井濒,可以有傳入參數,也可以有返回值
*/
public void algorithmInterface();
}
- 該來看看具體的算法實現了列林,定義了三個瑞你,分別是ConcreteStrategyA、ConcreteStrategyB希痴、ConcreteStrategyC捏悬,示例非常簡單,由于沒有具體算法的實現润梯,三者也就是名稱不同过牙,示例代碼如下:
/**
* 實現具體的算法
*/
public class ConcreteStrategyA implements Strategy {
public void algorithmInterface() {
//具體的算法實現
}
}
/**
* 實現具體的算法
*/
public class ConcreteStrategyB implements Strategy {
public void algorithmInterface() {
//具體的算法實現
}
}
/**
* 實現具體的算法
*/
public class ConcreteStrategyC implements Strategy {
public void algorithmInterface() {
//具體的算法實現
}
}
- 再來看看上下文的實現,示例代碼如下:
/**
* 上下文對象纺铭,通常會持有一個具體的策略對象
*/
public class Context {
/**
* 持有一個具體的策略對象
*/
private Strategy strategy;
/**
* 構造方法寇钉,傳入一個具體的策略對象
* @param aStrategy 具體的策略對象
*/
public Context(Strategy aStrategy) {
this.strategy = aStrategy;
}
/**
* 上下文對客戶端提供的操作接口,可以有參數和返回值
*/
public void contextInterface() {
//通常會轉調具體的策略對象進行算法運算
strategy.algorithmInterface();
}
}
2.4 使用策略模式重寫示例##
要使用策略模式來重寫前面報價的示例舶赔,大致有如下改變:
首先需要定義出算法的接口扫倡。
然后把各種報價的計算方式單獨出來,形成算法類竟纳。
對于Price這個類撵溃,把它當做上下文,在計算報價的時候锥累,不再需要判斷缘挑,直接使用持有的具體算法進行運算即可。選擇使用哪一個算法的功能挪出去桶略,放到外部使用的客戶端去语淘。
這個時候诲宇,程序的結構如圖所示:
- 先看策略接口,示例代碼如下:
/**
* 策略惶翻,定義計算報價算法的接口
*/
public interface Strategy {
/**
* 計算應報的價格
* @param goodsPrice 商品銷售原價
* @return 計算出來的姑蓝,應該給客戶報的價格
*/
public double calcPrice(double goodsPrice);
}
- 接下來看看具體的算法實現,不同的算法吕粗,實現也不一樣纺荧,先看為新客戶或者是普通客戶計算應報的價格的實現,示例代碼如下:
/**
* 具體算法實現颅筋,為新客戶或者是普通客戶計算應報的價格
*/
public class NormalCustomerStrategy implements Strategy{
public double calcPrice(double goodsPrice) {
System.out.println("對于新客戶或者是普通客戶宙暇,沒有折扣");
return goodsPrice;
}
}
再看看為老客戶計算應報的價格的實現,示例代碼如下:
/**
* 具體算法實現垃沦,為老客戶計算應報的價格
*/
public class OldCustomerStrategy implements Strategy{
public double calcPrice(double goodsPrice) {
System.out.println("對于老客戶客给,統(tǒng)一折扣5%");
return goodsPrice*(1-0.05);
}
}
再看看為大客戶計算應報的價格的實現用押,示例代碼如下:
/**
* 具體算法實現肢簿,為大客戶計算應報的價格
*/
public class LargeCustomerStrategy implements Strategy{
public double calcPrice(double goodsPrice) {
System.out.println("對于大客戶,統(tǒng)一折扣10%");
return goodsPrice*(1-0.1);
}
}
- 接下來看看上下文的實現蜻拨,也就是原來的價格類池充,它的變化比較大,主要有:
原來那些私有的缎讼,用來做不同計算的方法收夸,已經去掉了,獨立出去做成了算法類
原來報價方法里面血崭,對具體計算方式的判斷卧惜,去掉了,讓客戶端來完成選擇具體算法的功能
新添加持有一個具體的算法實現夹纫,通過構造方法傳入
原來報價方法的實現咽瓷,變化成了轉調具體算法來實現
示例代碼如下:
/**
* 價格管理,主要完成計算向客戶所報價格的功能
*/
public class Price {
/**
* 持有一個具體的策略對象
*/
private Strategy strategy = null;
/**
* 構造方法舰讹,傳入一個具體的策略對象
* @param aStrategy 具體的策略對象
*/
public Price(Strategy aStrategy){
this.strategy = aStrategy;
}
/**
* 報價茅姜,計算對客戶的報價
* @param goodsPrice 商品銷售原價
* @return 計算出來的,應該給客戶報的價格
*/
public double quote(double goodsPrice){
return this.strategy.calcPrice(goodsPrice);
}
}
- 寫個客戶端來測試運行一下月匣,好加深體會钻洒,示例代碼如下:
public class Client {
public static void main(String[] args) {
//1:選擇并創(chuàng)建需要使用的策略對象
Strategy strategy = new LargeCustomerStrategy ();
//2:創(chuàng)建上下文
Price ctx = new Price(strategy);
//3:計算報價
double quote = ctx.quote(1000);
System.out.println("向客戶報價:"+quote);
}
}
3 模式講解#
3.1 認識策略模式##
- 策略模式的功能
策略模式的功能是把具體的算法實現,從具體的業(yè)務處理里面獨立出來锄开,把它們實現成為單獨的算法類素标,從而形成一系列的算法,并讓這些算法可以相互替換萍悴。
策略模式的重心不是如何來實現算法糯钙,而是如何組織粪狼、調用這些算法,從而讓程序結構更靈活任岸、具有更好的維護性和擴展性再榄。
- 策略模式和if-else語句
看了前面的示例,很多朋友會發(fā)現享潜,每個策略算法具體實現的功能困鸥,就是原來在if-else結構中的具體實現。
沒錯剑按,其實多個if-elseif語句表達的就是一個平等的功能結構疾就,你要么執(zhí)行if,要不你就執(zhí)行else艺蝴,或者是elseif猬腰,這個時候,if塊里面的實現和else塊里面的實現從運行地位上來講就是平等的猜敢。
而策略模式就是把各個平等的具體實現封裝到單獨的策略實現類了姑荷,然后通過上下文來與具體的策略類進行交互。
因此多個if-else語句可以考慮使用策略模式缩擂。
- 算法的平等性
策略模式一個很大的特點就是各個策略算法的平等性鼠冕。對于一系列具體的策略算法,大家的地位是完全一樣的胯盯,正是因為這個平等性懈费,才能實現算法之間可以相互替換。
所有的策略算法在實現上也是相互獨立的博脑,相互之間是沒有依賴的憎乙。
所以可以這樣描述這一系列策略算法:策略算法是相同行為的不同實現。
- 誰來選擇具體的策略算法
在策略模式中叉趣,可以在兩個地方來進行具體策略的選擇泞边。
一個是在客戶端,在使用上下文的時候君账,由客戶端來選擇具體的策略算法繁堡,然后把這個策略算法設置給上下文。前面的示例就是這種情況乡数。
還有一個是客戶端不管椭蹄,由上下文來選擇具體的策略算法,這個在后面講容錯恢復的時候給大家演示一下净赴。
- Strategy的實現方式
在前面的示例中绳矩,Strategy都是使用的接口來定義的,這也是常見的實現方式玖翅。但是如果多個算法具有公共功能的話翼馆,可以把Strategy實現成為抽象類割以,然后把多個算法的公共功能實現到Strategy里面。
- 運行時策略的唯一性
運行期間应媚,策略模式在每一個時刻只能使用一個具體的策略實現對象严沥,雖然可以動態(tài)的在不同的策略實現中切換,但是同時只能使用一個中姜。
- 增加新的策略
在前面的示例里面消玄,體會到了策略模式中切換算法的方便,但是增加一個新的算法會怎樣呢丢胚?比如現在要實現如下的功能:對于公司的“戰(zhàn)略合作客戶”翩瓜,統(tǒng)一8折。
其實很簡單携龟,策略模式可以讓你很靈活的擴展新的算法兔跌。具體的做法是:先寫一個策略算法類來實現新的要求,然后在客戶端使用的時候指定使用新的策略算法類就可以了峡蟋。
還是通過示例來說明坟桅。先添加一個實現要求的策略類,示例代碼如下:
/**
* 具體算法實現层亿,為戰(zhàn)略合作客戶客戶計算應報的價格
*/
public class CooperateCustomerStrategy implements Strategy{
public double calcPrice(double goodsPrice) {
System.out.println("對于戰(zhàn)略合作客戶桦卒,統(tǒng)一8折");
return goodsPrice*0.8;
}
}
然后在客戶端指定使用策略的時候指定新的策略算法實現立美,示例如下:
public class Client2 {
public static void main(String[] args) {
//1:選擇并創(chuàng)建需要使用的策略對象
Strategy strategy = new CooperateCustomerStrategy ();
//2:創(chuàng)建上下文
Price ctx = new Price(strategy);
//3:計算報價
double quote = ctx.quote(1000);
System.out.println("向客戶報價:"+quote);
}
}
- 策略模式調用順序示意圖
策略模式的調用順序匿又,有兩種常見的情況,一種如同前面的示例建蹄,具體如下:
先是客戶端來選擇并創(chuàng)建具體的策略對象
然后客戶端創(chuàng)建上下文
接下來客戶端就可以調用上下文的方法來執(zhí)行功能了碌更,在調用的時候,從客戶端傳入算法需要的參數
上下文接到客戶的調用請求洞慎,會把這個請求轉發(fā)給它持有的Strategy
這種情況的調用順序示意圖如圖所示:
策略模式調用還有一種情況痛单,就是把Context當做參數來傳遞給Strategy
,這種方式的調用順序圖劲腿,在講具體的Context和Strategy的關系時再給出旭绒。
3.2 容錯恢復機制##
容錯恢復機制是應用程序開發(fā)中非常常見的功能。那么什么是容錯恢復呢焦人?簡單點說就是:程序運行的時候挥吵,正常情況下應該按照某種方式來做,如果按照某種方式來做發(fā)生錯誤的話花椭,系統(tǒng)并不會崩潰忽匈,也不會就此不能繼續(xù)向下運行了,而是有容忍出錯的能力矿辽,不但能容忍程序運行出現錯誤丹允,還提供出現錯誤后的備用方案郭厌,也就是恢復機制,來代替正常執(zhí)行的功能雕蔽,使程序繼續(xù)向下運行折柠。
舉個實際點的例子吧,比如在一個系統(tǒng)中批狐,所有對系統(tǒng)的操作都要有日志記錄液走,而且這個日志還需要有管理界面,這種情況下通常會把日志記錄在數據庫里面贾陷,方便后續(xù)的管理缘眶,但是在記錄日志到數據庫的時候,可能會發(fā)生錯誤髓废,比如暫時連不上數據庫了巷懈,那就先記錄在文件里面,然后在合適的時候把文件中的記錄再轉錄到數據庫中慌洪。
對于這樣的功能的設計顶燕,就可以采用策略模式,把日志記錄到數據庫和日志記錄到文件當作兩種記錄日志的策略冈爹,然后在運行期間根據需要進行動態(tài)的切換涌攻。
在這個例子的實現中,要示范由上下文來選擇具體的策略算法频伤,前面的例子都是由客戶端選擇好具體的算法恳谎,然后設置到上下文中
。
- 先定義日志策略接口憋肖,很簡單因痛,就是一個記錄日志的方法,示例代碼如下:
/**
* 日志記錄策略的接口
*/
public interface LogStrategy {
/**
* 記錄日志
* @param msg 需記錄的日志信息
*/
public void log(String msg);
}
- 實現日志策略接口岸更,先實現默認的數據庫實現憔晒,假設如果日志的長度超過長度就出錯钦勘,制造錯誤的是一個最常見的運行期錯誤址愿,示例代碼如下:
/**
* 把日志記錄到數據庫
*/
public class DbLog implements LogStrategy{
public void log(String msg) {
//制造錯誤
if(msg!=null && msg.trim().length()>5){
int a = 5/0;
}
System.out.println("現在把 '"+msg+"' 記錄到數據庫中");
}
}
接下來實現記錄日志到文件中去倔毙,示例代碼如下:
/**
* 把日志記錄到文件
*/
public class FileLog implements LogStrategy{
public void log(String msg) {
System.out.println("現在把 '"+msg+"' 記錄到文件中");
}
}
- 接下來定義使用這些策略的上下文,注意這次是在上下文里面實現具體策略算法的選擇评肆,所以不需要客戶端來指定具體的策略算法了债查,示例代碼如下:
- 看看現在的客戶端,沒有了選擇具體實現策略算法的工作糟港,變得非常簡單攀操,故意多調用一次,可以看出不同的效果秸抚,示例代碼如下:
- 小結一下速和,通過上面的示例歹垫,會看到策略模式的一種簡單應用,也順便了解一下基本的容錯恢復機制的設計和實現颠放。在實際的應用中排惨,需要設計容錯恢復的系統(tǒng)一般要求都比較高,應用也會比較復雜碰凶,但是基本的思路是差不多的暮芭。
3.3 Context和Strategy的關系##
在策略模式中,通常是上下文使用具體的策略實現對象欲低,反過來辕宏,策略實現對象也可以從上下文獲取所需要的數據,因此可以將上下文當參數傳遞給策略實現對象砾莱,這種情況下上下文和策略實現對象是緊密耦合的瑞筐。
在這種情況下,上下文封裝著具體策略對象進行算法運算所需要的數據腊瑟,具體策略對象通過回調上下文的方法來獲取這些數據聚假。
甚至在某些情況下,策略實現對象還可以回調上下文的方法來實現一定的功能闰非,這種使用場景下膘格,上下文變相充當了多個策略算法實現的公共接口,在上下文定義的方法可以當做是所有或者是部分策略算法使用的公共功能财松。
但是請注意瘪贱,由于所有的策略實現對象都實現同一個策略接口,傳入同一個上下文游岳,可能會造成傳入的上下文數據的浪費政敢,因為有的算法會使用這些數據其徙,而有的算法不會使用胚迫,但是上下文和策略對象之間交互的開銷是存在的了。
還是通過例子來說明唾那。
- 工資支付的實現思路
考慮這樣一個功能:工資支付方式的問題访锻,很多企業(yè)的工資支付方式是很靈活的,可支付方式是比較多的闹获,比如:人民幣現金支付期犬、美元現金支付、銀行轉賬到工資帳戶避诽、銀行轉賬到工資卡龟虎;一些創(chuàng)業(yè)型的企業(yè)為了留住骨干員工,還可能有:工資轉股權等等方式沙庐±鹜祝總之一句話佳吞,工資支付方式很多。
隨著公司的發(fā)展棉安,會不斷有新的工資支付方式出現底扳,這就要求能方便的擴展;另外工資支付方式不是固定的贡耽,是由公司和員工協(xié)商確定的衷模,也就是說可能不同的員工采用的是不同的支付方式,甚至同一個員工蒲赂,不同時間采用的支付方式也可能會不同阱冶,這就要求能很方便的切換具體的支付方式。
要實現這樣的功能滥嘴,策略模式是一個很好的選擇熙揍。在實現這個功能的時候,不同的策略算法需要的數據是不一樣氏涩,比如:現金支付就不需要銀行帳號届囚,而銀行轉賬就需要帳號。這就導致在設計策略接口中的方法時是尖,不太好確定參數的個數意系,而且,就算現在把所有的參數都列上了饺汹,今后擴展呢蛔添?難道再來修改策略接口嗎?如果這樣做兜辞,那無異于一場災難迎瞧,加入一個新策略,就需要修改接口逸吵,然后修改所有已有的實現凶硅,不瘋掉才怪!那么到底如何實現扫皱,在今后擴展的時候才最方便呢足绅?
解決方案之一,就是把上下文當做參數傳遞給策略對象韩脑,這樣一來氢妈,如果要擴展新的策略實現,只需要擴展上下文就可以了段多,已有的實現不需要做任何的修改首量。
這樣是不是能很好的實現功能,并具有很好的擴展性呢?還是通過代碼示例來具體的看加缘。假設先實現人民幣現金支付和美元現金支付這兩種支付方式粥航,然后就進行使用測試,然后再來添加銀行轉賬到工資卡的支付方式生百,看看是不是能很容易的與已有的實現結合上递雀。
- 實現代碼示例
(1)先定義工資支付的策略接口,就是定義一個支付工資的方法蚀浆,示例代碼如下:
/**
* 支付工資的策略的接口缀程,公司有多種支付工資的算法
* 比如:現金、銀行卡市俊、現金加股票杨凑、現金加期權、美元支付等等
*/
public interface PaymentStrategy {
/**
* 公司給某人真正支付工資
* @param ctx 支付工資的上下文摆昧,里面包含算法需要的數據
*/
public void pay(PaymentContext ctx);
}
(2)定義好了工資支付的策略接口撩满,該來考慮如何實現這多種支付策略了。
為了演示的簡單绅你,這里先簡單實現人民幣現金支付和美元現金支付方式伺帘,當然并不真的去實現跟銀行的交互,只是示意一下忌锯。
人民幣現金支付的策略實現伪嫁,示例代碼如下:
/**
* 人民幣現金支付
*/
public class RMBCash implements PaymentStrategy{
public void pay(PaymentContext ctx) {
System.out.println("現在給"+ctx.getUserName()+"人民幣現金支付"+ctx.getMoney()+"元");
}
}
同樣的實現美元現金支付的策略,示例代碼如下:
/**
* 美元現金支付
*/
public class DollarCash implements PaymentStrategy{
public void pay(PaymentContext ctx) {
System.out.println("現在給"+ctx.getUserName()+"美元現金支付"+ctx.getMoney()+"元");
}
}
(3)該來看支付上下文的實現了偶垮,當然這個使用支付策略的上下文张咳,是需要知道具體使用哪一個支付策略的,一般由客戶端來確定具體使用哪一個具體的策略似舵,然后上下文負責去真正執(zhí)行脚猾。因此,這個上下文需要持有一個支付策略砚哗,而且是由客戶端來配置它龙助。示例代碼如下:
/**
* 支付工資的上下文,每個人的工資不同频祝,支付方式也不同
*/
public class PaymentContext {
/**
* 應被支付工資的人員泌参,簡單點,用姓名來代替
*/
private String userName = null;
/**
* 應被支付的工資的金額
*/
private double money = 0.0;
/**
* 支付工資的方式策略的接口
*/
private PaymentStrategy strategy = null;
/**
* 構造方法常空,傳入被支付工資的人員,應支付的金額和具體的支付策略
* @param userName 被支付工資的人員
* @param money 應支付的金額
* @param strategy 具體的支付策略
*/
public PaymentContext(String userName,double money,PaymentStrategy strategy){
this.userName = userName;
this.money = money;
this.strategy = strategy;
}
public String getUserName() {
return userName;
}
public double getMoney() {
return money;
}
/**
* 立即支付工資
*/
public void payNow(){
//使用客戶希望的支付策略來支付工資
this.strategy.pay(this);
}
}
(4)準備好了支付工資的各種策略盖溺,下面看看如何使用這些策略來真正支付工資漓糙,很簡單,客戶端是使用上下文來使用具體的策略的烘嘱,而且是客戶端來確定具體的策略昆禽,就是客戶端創(chuàng)建哪個策略蝗蛙,最終就運行哪一個策略,各個策略之間是可以動態(tài)切換的醉鳖,示例代碼如下:
public class Client {
public static void main(String[] args) {
//創(chuàng)建相應的支付策略
PaymentStrategy strategyRMB = new RMBCash();
PaymentStrategy strategyDollar = new DollarCash();
//準備小李的支付工資上下文
PaymentContext ctx1 = new PaymentContext("小李",5000,strategyRMB);
//向小李支付工資
ctx1.payNow();
//切換一個人捡硅,給petter支付工資
PaymentContext ctx2 = new PaymentContext("Petter",8000,strategyDollar);
ctx2.payNow();
}
}
- 擴展示例,實現方式一
經過上面的測試可以看出盗棵,通過使用策略模式壮韭,已經實現好了兩種支付方式了。如果現在要增加一種支付方式纹因,要求能支付到銀行卡喷屋,該怎么擴展最簡單呢?
應該新增加一種支付到銀行卡的策略實現瞭恰,然后通過繼承來擴展支付上下文屯曹,在里面添加新的支付方式需要的新的數據,比如銀行卡賬戶惊畏,然后在客戶端使用新的上下文和新的策略實現就可以了恶耽,這樣已有的實現都不需要改變,完全遵循開-閉原則颜启。
先看看擴展的支付上下文對象的實現驳棱,示例代碼如下:
/**
* 擴展的支付上下文對象
*/
public class PaymentContext2 extends PaymentContext {
/**
* 銀行帳號
*/
private String account = null;
/**
* 構造方法,傳入被支付工資的人員农曲,應支付的金額和具體的支付策略
* @param userName 被支付工資的人員
* @param money 應支付的金額
* @param account 支付到的銀行帳號
* @param strategy 具體的支付策略
*/
public PaymentContext2(String userName,double money,String account,PaymentStrategy strategy){
super(userName,money,strategy);
this.account = account;
}
public String getAccount() {
return account;
}
}
然后看看新的策略算法的實現社搅,示例代碼如下:
/**
* 支付到銀行卡
*/
public class Card implements PaymentStrategy{
public void pay(PaymentContext ctx) {
// 這個新的算法自己知道要使用擴展的支付上下文,所以強制造型一下
PaymentContext2 ctx2 = (PaymentContext2)ctx;
System.out.println(" 現在給 "+ctx2.getUserName()+" 的 "+ctx2.getAccount()+" 帳號支付了 "+ctx2.getMoney()+" 元 ");
// 連接銀行乳规,進行轉帳形葬,就不去管了
}
}
最后看看客戶端怎么使用這個新的策略呢?原有的代碼不變暮的,直接添加新的測試就可以了笙以,示例代碼如下:
public class Client {
public static void main(String[] args) {
//創(chuàng)建相應的支付策略
PaymentStrategy strategyRMB = new RMBCash();
PaymentStrategy strategyDollar = new DollarCash();
//準備小李的支付工資上下文
PaymentContext ctx1 = new PaymentContext("小李 ",5000,strategyRMB);
//向小李支付工資
ctx1.payNow();
//切換一個人,給 petter支付工資
PaymentContext ctx2 = new PaymentContext("Petter",8000,strategyDollar);
ctx2.payNow();
// 測試新添加的支付方式
PaymentStrategy strategyCard = new Card();
PaymentContext ctx3 = new PaymentContext2("小王",9000,"010998877656",strategyCard);
ctx3.payNow();
}
}
- 擴展示例冻辩,實現方式二
(1)上面這種實現方式猖腕,是通過擴展上下文對象來準備新的算法需要的數據。還有另外一種方式恨闪,那就是通過策略的構造方法來傳入新算法需要的數據倘感。這樣實現的話,就不需要擴展上下文了咙咽,直接添加新的策略算法實現就好了老玛。示例代碼如下:
/**
* 支付到銀行卡
*/
public class Card2 implements PaymentStrategy{
/**
* 帳號信息
*/
private String account = "";
/**
* 構造方法,傳入帳號信息
* @param account 帳號信息
*/
public Card2(String account){
this.account = account;
}
public void pay(PaymentContext ctx) {
System.out.println(" 現在給 "+ctx.getUserName()+" 的 "+this.account+" 帳號支付了 "+ctx.getMoney()+" 元 ");
// 連接銀行,進行轉帳蜡豹,就不去管了
}
}
(2)直接在客戶端測試就可以了麸粮,測試示例代碼如下:
public class Client {
public static void main(String[] args) {
//測試新添加的支付方式
PaymentStrategy strategyCard2 = new Card2("010998877656");
PaymentContext ctx4 = new PaymentContext("小張",9000,strategyCard2);
ctx4.payNow();
}
}
(3)現在有這么兩種擴展的實現方式,到底使用哪一種呢镜廉?或者是哪種實現更好呢弄诲?下面來比較一下:
對于擴展上下文的方式:
這樣實現,所有策略的實現風格更統(tǒng)一娇唯,策略需要的數據都統(tǒng)一從上下文來獲取齐遵,這樣在使用方法上也很統(tǒng)一;另外视乐,在上下文中添加新的數據洛搀,別的相應算法也可以用得上,可以視為公共的數據佑淀。但缺點也很明顯留美,如果這些數據只有一個特定的算法來使用,那么這些數據有些浪費伸刃;另外每次添加新的算法都去擴展上下文谎砾,容易形成復雜的上下文對象層次,也未見得有必要捧颅。
對于在策略算法的實現上添加自己需要的數據的方式:
這樣實現景图,比較好想,實現簡單碉哑。但是缺點也很明顯挚币,跟其它策略實現的風格不一致,其它策略都是從上下文中來獲取數據扣典,而這個策略的實現一部分數據來自上下文妆毕,一部分數據來自自己,有些不統(tǒng)一贮尖;另外笛粘,這樣一來,外部使用這些策略算法的時候也不一樣了湿硝,不太好以一個統(tǒng)一的方式來動態(tài)切換策略算法薪前。
兩種實現各有優(yōu)劣,至于如何選擇关斜,那就具體問題示括,具體的分析了。
- 另一種策略模式調用順序示意圖
策略模式調用還有一種情況蚤吹,就是把Context當做參數來傳遞給Strategy例诀,也就是本例示范的這種方式随抠,這個時候策略模式的調用順序如圖所示:
3.4 策略模式結合模板方法模式##
在實際應用策略模式的過程中裁着,經常會出現這樣一種情況繁涂,就是發(fā)現這一系列算法的實現上存在公共功能,甚至這一系列算法的實現步驟都是一樣的二驰,只是在某些局部步驟上有所不同
扔罪,這個時候,就需要對策略模式進行些許的變化使用了桶雀。
對于一系列算法的實現上存在公共功能的情況矿酵,策略模式可以有如下三種實現方式:
一個是在上下文當中實現公共功能,讓所有具體的策略算法回調這些方法矗积。
另外一種情況就是把策略的接口改成抽象類全肮,然后在里面實現具體算法的公共功能。
還有一種情況是給所有的策略算法定義一個抽象的父類棘捣,讓這個父類去實現策略的接口辜腺,然后在這個父類里面去實現公共的功能。
更進一步乍恐,如果這個時候發(fā)現“一系列算法的實現步驟都是一樣的评疗,只是在某些局部步驟上有所不同”的情況,那就可以在這個抽象類里面定義算法實現的骨架茵烈,然后讓具體的策略算法去實現變化的部分百匆。這樣的一個結構自然就變成了策略模式來結合模板方法模式了,那個抽象類就成了模板方法模式的模板類
呜投。
我們討論過模板方法模式來結合策略模式的方式加匈,也就是主要的結構是模板方法模式,局部采用策略模式
仑荐。而這里討論的是策略模式來結合模板方法模式雕拼,也就是主要的結構是策略模式,局部實現上采用模板方法模式
释漆。通過這個示例也可以看出來悲没,模式之間的結合是沒有定勢的,要具體問題具體分析男图。
此時策略模式結合模板方法模式的系統(tǒng)結構如下圖所示:
還是用實際的例子來說吧示姿,比如上面那個記錄日志的例子,如果現在需要在所有的消息前面都添加上日志時間逊笆,也就是說現在記錄日志的步驟變成了:第一步為日志消息添加日志時間栈戳;第二步具體記錄日志。
那么該怎么實現呢难裆?
- 記錄日志的策略接口沒有變化子檀,為了看起來方便镊掖,還是示例一下,示例代碼如下:
/**
* 日志記錄策略的接口
*/
public interface LogStrategy {
/**
* 記錄日志
* @param msg 需記錄的日志信息
*/
public void log(String msg);
}
- 增加一個實現這個策略接口的抽象類褂痰,在里面定義記錄日志的算法骨架亩进,相當于模板方法模式的模板,示例代碼如下:
/**
* 實現日志策略的抽象模板缩歪,實現給消息添加時間
*/
public abstract class LogStrategyTemplate implements LogStrategy {
public final void log(String msg) {
//第一步:給消息添加記錄日志的時間
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
msg = df.format(new java.util.Date())+" 內容是:"+ msg;
//第二步:真正執(zhí)行日志記錄
doLog(msg);
}
/**
* 真正執(zhí)行日志記錄归薛,讓子類去具體實現
* @param msg 需記錄的日志信息
*/
protected abstract void doLog(String msg);
}
- 這個時候那兩個具體的日志算法實現也需要做些改變,不再直接實現策略接口了匪蝙,而是繼承模板主籍,實現模板方法了。這個時候記錄日志到數據庫的類逛球,示例代碼如下:
/**
* 把日志記錄到數據庫
*/
public class DbLog extends LogStrategyTemplate{
//除了定義上發(fā)生了改變外千元,具體的實現沒變
public void doLog(String msg) {
//制造錯誤
if(msg!=null && msg.trim().length()>5){
int a = 5/0;
}
System.out.println("現在把 '"+msg+"' 記錄到數據庫中");
}
}
同理實現記錄日志到文件的類如下:
/**
* 把日志記錄到數據庫
*/
public class FileLog extends LogStrategyTemplate{
public void doLog(String msg) {
System.out.println("現在把 '"+msg+"' 記錄到文件中");
}
}
- 算法實現的改變不影響使用算法的上下文,上下文跟前面一樣颤绕,示例代碼如下:
/**
* 日志記錄的上下文
*/
public class LogContext {
/**
* 記錄日志的方法幸海,提供給客戶端使用
* @param msg 需記錄的日志信息
*/
public void log(String msg){
//在上下文里面,自行實現對具體策略的選擇
//優(yōu)先選用策略:記錄到數據庫
LogStrategy strategy = new DbLog();
try{
strategy.log(msg);
}catch(Exception err){
//出錯了屋厘,那就記錄到文件中
strategy = new FileLog();
strategy.log(msg);
}
}
}
- 客戶端跟以前也一樣涕烧,示例代碼如下:
public class Client {
public static void main(String[] args) {
LogContext log = new LogContext();
log.log("記錄日志");
log.log("再次記錄日志");
}
}
3.5 策略模式的優(yōu)缺點##
- 定義一系列算法
策略模式的功能就是定義一系列算法,實現讓這些算法可以相互替換汗洒。所以會為這一系列算法定義公共的接口议纯,以約束一系列算法要實現的功能。如果這一系列算法具有公共功能溢谤,可以把策略接口實現成為抽象類瞻凤,把這些公共功能實現到父類里面,對于這個問題世杀,前面講了三種處理方法阀参,這里就不羅嗦了。
- 避免多重條件語句
根據前面的示例會發(fā)現瞻坝,策略模式的一系列策略算法是平等的蛛壳,可以互換的,寫在一起就是通過if-else結構來組織所刀,如果此時具體的算法實現里面又有條件語句衙荐,就構成了多重條件語句,使用策略模式能避免這樣的多重條件語句浮创。
- 更好的擴展性
在策略模式中擴展新的策略實現非常容易忧吟,只要增加新的策略實現類,然后在選擇使用策略的地方選擇使用這個新的策略實現就好了斩披。
- 客戶必須了解每種策略的不同
策略模式也有缺點溜族,比如讓客戶端來選擇具體使用哪一個策略讹俊,這就可能會讓客戶需要了解所有的策略,還要了解各種策略的功能和不同煌抒,這樣才能做出正確的選擇仍劈,而且這樣也暴露了策略的具體實現。
- 增加了對象數目
由于策略模式把每個具體的策略實現都單獨封裝成為類摧玫,如果備選的策略很多的話耳奕,那么對象的數目就會很可觀绑青。
- 只適合扁平的算法結構
策略模式的一系列算法地位是平等的诬像,是可以相互替換的,事實上構成了一個扁平的算法結構闸婴,也就是在一個策略接口下坏挠,有多個平等的策略算法,就相當于兄弟算法邪乍。而且在運行時刻只有一個算法被使用降狠,這就限制了算法使用的層級,使用的時候不能嵌套使用庇楞。
對于出現需要嵌套使用多個算法的情況榜配,比如折上折、折后返卷等業(yè)務的實現吕晌,需要組合或者是嵌套使用多個算法的情況蛋褥,可以考慮使用裝飾模式、或是變形的職責鏈睛驳、或是AOP等方式來實現
烙心。
3.6 思考策略模式##
- 策略模式的本質
策略模式的本質:分離算法,選擇實現乏沸。
仔細思考策略模式的結構和實現的功能淫茵,會發(fā)現,如果沒有上下文蹬跃,策略模式就回到了最基本的接口和實現了匙瘪,只要是面向接口編程的,那么就能夠享受到接口的封裝隔離帶來的好處蝶缀。也就是通過一個統(tǒng)一的策略接口來封裝和隔離具體的策略算法丹喻,面向接口編程的話,自然不需要關心具體的策略實現扼劈,也可以通過使用不同的實現類來實例化接口驻啤,從而實現切換具體的策略。
看起來好像沒有上下文什么事情荐吵,但是如果沒有上下文骑冗,那么就需要客戶端來直接與具體的策略交互赊瞬,尤其是當需要提供一些公共功能,或者是相關狀態(tài)存儲的時候贼涩,會大大增加客戶端使用的難度巧涧。因此,引入上下文還是很必要的遥倦,有了上下文谤绳,這些工作就由上下文來完成了,客戶端只需要與上下文交互就可以了袒哥,這樣會讓整個設計模式更獨立缩筛、更有整體性,也讓客戶端更簡單堡称。
但縱觀整個策略模式實現的功能和設計瞎抛,它的本質還是“分離算法,選擇實現”却紧,因為分離并封裝了算法桐臊,才能夠很容易的修改和添加算法;也能很容易的動態(tài)切換使用不同的算法晓殊,也就是動態(tài)選擇一個算法來實現需要的功能了断凶。
- 對設計原則的體現
從設計原則上來看,策略模式很好的體現了開-閉原則巫俺。策略模式通過把一系列可變的算法進行封裝认烁,并定義出合理的使用結構,使得在系統(tǒng)出現新算法的時候识藤,能很容易的把新的算法加入到已有的系統(tǒng)中砚著,而已有的實現不需要做任何修改。這在前面的示例中已經體現出來了痴昧,好好體會一下稽穆。
從設計原則上來看,策略模式還很好的體現了里氏替換原則赶撰。策略模式是一個扁平結構舌镶,一系列的實現算法其實是兄弟關系,都是實現同一個接口或者繼承的同一個父類豪娜。這樣只要使用策略的客戶保持面向抽象類型編程餐胀,就能夠使用不同的策略的具體實現對象來配置它,從而實現一系列算法可以相互替換瘤载。
- 何時選用策略模式
建議在如下情況中否灾,選用策略模式:
出現有許多相關的類,僅僅是行為有差別的情況鸣奔,可以使用策略模式來使用多個行為中的一個來配置一個類的方法墨技,實現算法動態(tài)切換
出現同一個算法惩阶,有很多不同的實現的情況,可以使用策略模式來把這些“不同的實現”實現成為一個算法的類層次
需要封裝算法中扣汪,與算法相關的數據的情況断楷,可以使用策略模式來避免暴露這些跟算法相關的數據結構
出現抽象一個定義了很多行為的類,并且是通過多個if-else語句來選擇這些行為的情況崭别,可以使用策略模式來代替這些條件語句
3.7 相關模式##
- 策略模式和狀態(tài)模式
這兩個模式從模式結構上看是一樣的冬筒,但是實現的功能是不一樣的。
狀態(tài)模式是根據狀態(tài)的變化來選擇相應的行為茅主,不同的狀態(tài)對應不同的類舞痰,每個狀態(tài)對應的類實現了該狀態(tài)對應的功能,在實現功能的同時暗膜,還會維護狀態(tài)數據的變化匀奏。這些實現狀態(tài)對應的功能的類之間是不能相互替換的。
策略模式是根據需要或者是客戶端的要求來選擇相應的實現類学搜,各個實現類是平等的,是可以相互替換的论衍。
另外策略模式可以讓客戶端來選擇需要使用的策略算法瑞佩,而狀態(tài)模式一般是由上下文,或者是在狀態(tài)實現類里面來維護具體的狀態(tài)數據坯台,通常不由客戶端來指定狀態(tài)炬丸。
- 策略模式和模板方法模式
這兩個模式可組合使用,如同前面示例的那樣蜒蕾。
模板方法重在封裝算法骨架稠炬,而策略模式重在分離并封裝算法實現。
- 策略模式和享元模式
這兩個模式可組合使用咪啡。
策略模式分離并封裝出一系列的策略算法對象首启,這些對象的功能通常都比較單一,很多時候就是為了實現某個算法的功能而存在撤摸,因此毅桃,針對這一系列的、多個細粒度的對象准夷,可以應用享元模式來節(jié)省資源捌朴,但前提是這些算法對象要被頻繁的使用定续,如果偶爾用一次,就沒有必要做成享元了。