1 場景問題#
1.1 報價管理##
向客戶報價,對于銷售部門的人來講,這是一個非常重大、非常復雜的問題,對不同的客戶要報不同的價格,比如:
- 對普通客戶或者是新客戶報的是全價
- 對老客戶報的價格,根據(jù)客戶年限,給予一定的折扣
- 對大客戶報的價格,根據(jù)大客戶的累計消費金額,給予一定的折扣
還要考慮客戶購買的數(shù)量和金額,比如:雖然是新用戶,但是一次購買的數(shù)量非常大,或者是總金額非常高,也會有一定的折扣
還有,報價人員的職務(wù)高低,也決定了他是否有權(quán)限對價格進行一定的浮動折扣
甚至在不同的階段,對客戶的報價也不同,一般情況是剛開始比較高,越接近成交階段,報價越趨于合理。
總之,向客戶報價是非常復雜的,因此在一些CRM(客戶關(guān)系管理)的系統(tǒng)中,會有一個單獨的報價管理模塊,來處理復雜的報價功能。
為了演示的簡潔性,假定現(xiàn)在需要實現(xiàn)一個簡化的報價管理,實現(xiàn)如下的功能:
(1)對普通客戶或者是新客戶報全價
(2)對老客戶報的價格,統(tǒng)一折扣5%
(3)對大客戶報的價格,統(tǒng)一折扣10%
該怎么實現(xiàn)呢?
1.2 不用模式的解決方案##
要實現(xiàn)對不同的人員報不同的價格的功能,無外乎就是判斷起來麻煩點,也不多難,很快就有朋友能寫出如下的實現(xiàn)代碼,示例代碼如下:
/**
* 價格管理,主要完成計算向客戶所報價格的功能
*/
public class Price {
/**
* 報價,對不同類型的,計算不同的價格
* @param goodsPrice 商品銷售原價
* @param customerType 客戶類型
* @return 計算出來的,應(yīng)該給客戶報的價格
*/
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 有何問題##
上面的寫法是很簡單的,也很容易想,但是仔細想想,這樣實現(xiàn),問題可不小,比如:第一個問題:價格類包含了所有計算報價的算法,使得價格類,尤其是報價這個方法比較龐雜,難以維護。
有朋友可能會想,這很簡單嘛,把這些算法從報價方法里面拿出去,形成獨立的方法不就可以解決這個問題了嗎?據(jù)此寫出如下的實現(xiàn)代碼,示例代碼如下:
/**
* 價格管理,主要完成計算向客戶所報價格的功能
*/
public class Price {
/**
* 報價,對不同類型的,計算不同的價格
* @param goodsPrice 商品銷售原價
* @param customerType 客戶類型
* @return 計算出來的,應(yīng)該給客戶報的價格
*/
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;
}
/**
* 為新客戶或者是普通客戶計算應(yīng)報的價格
* @param goodsPrice 商品銷售原價
* @return 計算出來的,應(yīng)該給客戶報的價格
*/
private double calcPriceForNormal(double goodsPrice){
System.out.println("對于新客戶或者是普通客戶,沒有折扣 ");
return goodsPrice;
}
/**
* 為老客戶計算應(yīng)報的價格
* @param goodsPrice 商品銷售原價
* @return 計算出來的,應(yīng)該給客戶報的價格
*/
private double calcPriceForOld(double goodsPrice){
System.out.println("對于老客戶,統(tǒng)一折扣 5%");
return goodsPrice*(1-0.05);
}
/**
* 為大客戶計算應(yīng)報的價格
* @param goodsPrice 商品銷售原價
* @return 計算出來的,應(yīng)該給客戶報的價格
*/
private double calcPriceForLarge(double goodsPrice){
System.out.println("對于大客戶,統(tǒng)一折扣 10%");
return goodsPrice*(1-0.1);
}
}
這樣看起來,比剛開始稍稍好點,計算報價的方法會稍稍簡單一點,這樣維護起來也稍好一些,某個算法發(fā)生了變化,直接修改相應(yīng)的私有方法就可以了。擴展起來也容易一點,比如要增加一個“戰(zhàn)略合作客戶”的類型,報價為直接8折,就只需要在價格類里面新增加一個私有的方法來計算新的價格,然后在計算報價的方法里面新添一個else-if即可。看起來似乎很不錯了。
真的很不錯了嗎?
再想想,問題還是存在,只不過從計算報價的方法挪動到價格類里面了,假如有100個或者更多這樣的計算方式,這會讓這個價格類非常龐大,難以維護。而且,維護和擴展都需要去修改已有的代碼,這是很不好的,違反了開-閉原則。
第二個問題:經(jīng)常會有這樣的需要,在不同的時候,要使用不同的計算方式。
比如:在公司周年慶的時候,所有的客戶額外增加3%的折扣;在換季促銷的時候,普通客戶是額外增加折扣2%,老客戶是額外增加折扣3%,大客戶是額外增加折扣5%。這意味著計算報價的方式會經(jīng)常被修改,或者被切換。
通常情況下應(yīng)該是被切換,因為過了促銷時間,又還回到正常的價格體系上來了。而現(xiàn)在的價格類中計算報價的方法,是固定調(diào)用各種計算方式,這使得切換調(diào)用不同的計算方式很麻煩,每次都需要修改if-else里面的調(diào)用代碼。
看到這里,可能有朋友會想, 那么到底應(yīng)該如何實現(xiàn),才能夠讓價格類中的計算報價的算法,能很容易的實現(xiàn)可維護、可擴展,又能動態(tài)的切換變化呢?
2 解決方案#
2.1 策略模式來解決##
用來解決上述問題的一個合理的解決方案就是策略模式。那么什么是策略模式呢?
- 策略模式定義
定義一系列的算法,把它們一個個封裝起來,并且使它們可相互替換。本模式使得算法可獨立于使用它的客戶而變化。
- 應(yīng)用策略模式來解決的思路
仔細分析上面的問題,先來把它抽象一下,各種計算報價的計算方式就好比是具體的算法,而使用這些計算方式來計算報價的程序,就相當于是使用算法的客戶。
再分析上面的實現(xiàn)方式,為什么會造成那些問題,根本原因,就在于算法和使用算法的客戶是耦合的,甚至是密不可分的,在上面實現(xiàn)中,具體的算法和使用算法的客戶是同一個類里面的不同方法。
現(xiàn)在要解決那些問題,按照策略模式的方式,應(yīng)該先把所有的計算方式獨立出來,每個計算方式做成一個單獨的算法類,從而形成一系列的算法,并且為這一系列算法定義一個公共的接口,這些算法實現(xiàn)是同一接口的不同實現(xiàn),地位是平等的,可以相互替換。這樣一來,要擴展新的算法就變成了增加一個新的算法實現(xiàn)類,要維護某個算法,也只是修改某個具體的算法實現(xiàn)即可,不會對其它代碼造成影響。也就是說這樣就解決了可維護、可擴展的問題。
為了實現(xiàn)讓算法能獨立于使用它的客戶,策略模式引入了一個上下文的對象,這個對象負責持有算法,但是不負責決定具體選用哪個算法,把選擇算法的功能交給了客戶,由客戶選擇好具體的算法后,設(shè)置到上下文對象里面,讓上下文對象持有客戶選擇的算法,當客戶通知上下文對象執(zhí)行功能的時候,上下文對象會去轉(zhuǎn)調(diào)具體的算法。這樣一來,具體的算法和直接使用算法的客戶是分離的。
具體的算法和使用它的客戶分離過后,使得算法可獨立于使用它的客戶而變化,并且能夠動態(tài)的切換需要使用的算法,只要客戶端動態(tài)的選擇使用不同的算法,然后設(shè)置到上下文對象中去,實際調(diào)用的時候,就可以調(diào)用到不同的算法。
2.2 模式結(jié)構(gòu)和說明##
策略模式的結(jié)構(gòu)示意圖如圖所示:

Strategy:策略接口,用來約束一系列具體的策略算法。Context使用這個接口來調(diào)用具體的策略實現(xiàn)定義的算法。
ConcreteStrategy:具體的策略實現(xiàn),也就是具體的算法實現(xiàn)。
Context:上下文,負責和具體的策略類交互,通常上下文會持有一個真正的策略實現(xiàn),上下文還可以讓具體的策略類來獲取上下文的數(shù)據(jù),甚至讓具體的策略類來回調(diào)上下文的方法。
2.3 策略模式示例代碼##
- 首先來看策略,也就是定義算法的接口,示例代碼如下:
/**
* 策略,定義算法的接口
*/
public interface Strategy {
/**
* 某個算法的接口,可以有傳入?yún)?shù),也可以有返回值
*/
public void algorithmInterface();
}
- 該來看看具體的算法實現(xiàn)了,定義了三個,分別是ConcreteStrategyA、ConcreteStrategyB、ConcreteStrategyC,示例非常簡單,由于沒有具體算法的實現(xiàn),三者也就是名稱不同,示例代碼如下:
/**
* 實現(xiàn)具體的算法
*/
public class ConcreteStrategyA implements Strategy {
public void algorithmInterface() {
//具體的算法實現(xiàn)
}
}
/**
* 實現(xiàn)具體的算法
*/
public class ConcreteStrategyB implements Strategy {
public void algorithmInterface() {
//具體的算法實現(xiàn)
}
}
/**
* 實現(xiàn)具體的算法
*/
public class ConcreteStrategyC implements Strategy {
public void algorithmInterface() {
//具體的算法實現(xiàn)
}
}
- 再來看看上下文的實現(xiàn),示例代碼如下:
/**
* 上下文對象,通常會持有一個具體的策略對象
*/
public class Context {
/**
* 持有一個具體的策略對象
*/
private Strategy strategy;
/**
* 構(gòu)造方法,傳入一個具體的策略對象
* @param aStrategy 具體的策略對象
*/
public Context(Strategy aStrategy) {
this.strategy = aStrategy;
}
/**
* 上下文對客戶端提供的操作接口,可以有參數(shù)和返回值
*/
public void contextInterface() {
//通常會轉(zhuǎn)調(diào)具體的策略對象進行算法運算
strategy.algorithmInterface();
}
}
2.4 使用策略模式重寫示例##
要使用策略模式來重寫前面報價的示例,大致有如下改變:
首先需要定義出算法的接口。
然后把各種報價的計算方式單獨出來,形成算法類。
對于Price這個類,把它當做上下文,在計算報價的時候,不再需要判斷,直接使用持有的具體算法進行運算即可。選擇使用哪一個算法的功能挪出去,放到外部使用的客戶端去。
這個時候,程序的結(jié)構(gòu)如圖所示:

- 先看策略接口,示例代碼如下:
/**
* 策略,定義計算報價算法的接口
*/
public interface Strategy {
/**
* 計算應(yīng)報的價格
* @param goodsPrice 商品銷售原價
* @return 計算出來的,應(yīng)該給客戶報的價格
*/
public double calcPrice(double goodsPrice);
}
- 接下來看看具體的算法實現(xiàn),不同的算法,實現(xiàn)也不一樣,先看為新客戶或者是普通客戶計算應(yīng)報的價格的實現(xiàn),示例代碼如下:
/**
* 具體算法實現(xiàn),為新客戶或者是普通客戶計算應(yīng)報的價格
*/
public class NormalCustomerStrategy implements Strategy{
public double calcPrice(double goodsPrice) {
System.out.println("對于新客戶或者是普通客戶,沒有折扣");
return goodsPrice;
}
}
再看看為老客戶計算應(yīng)報的價格的實現(xiàn),示例代碼如下:
/**
* 具體算法實現(xiàn),為老客戶計算應(yīng)報的價格
*/
public class OldCustomerStrategy implements Strategy{
public double calcPrice(double goodsPrice) {
System.out.println("對于老客戶,統(tǒng)一折扣5%");
return goodsPrice*(1-0.05);
}
}
再看看為大客戶計算應(yīng)報的價格的實現(xiàn),示例代碼如下:
/**
* 具體算法實現(xiàn),為大客戶計算應(yīng)報的價格
*/
public class LargeCustomerStrategy implements Strategy{
public double calcPrice(double goodsPrice) {
System.out.println("對于大客戶,統(tǒng)一折扣10%");
return goodsPrice*(1-0.1);
}
}
- 接下來看看上下文的實現(xiàn),也就是原來的價格類,它的變化比較大,主要有:
原來那些私有的,用來做不同計算的方法,已經(jīng)去掉了,獨立出去做成了算法類
原來報價方法里面,對具體計算方式的判斷,去掉了,讓客戶端來完成選擇具體算法的功能
新添加持有一個具體的算法實現(xiàn),通過構(gòu)造方法傳入
原來報價方法的實現(xiàn),變化成了轉(zhuǎn)調(diào)具體算法來實現(xiàn)
示例代碼如下:
/**
* 價格管理,主要完成計算向客戶所報價格的功能
*/
public class Price {
/**
* 持有一個具體的策略對象
*/
private Strategy strategy = null;
/**
* 構(gòu)造方法,傳入一個具體的策略對象
* @param aStrategy 具體的策略對象
*/
public Price(Strategy aStrategy){
this.strategy = aStrategy;
}
/**
* 報價,計算對客戶的報價
* @param goodsPrice 商品銷售原價
* @return 計算出來的,應(yīng)該給客戶報的價格
*/
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 認識策略模式##
- 策略模式的功能
策略模式的功能是把具體的算法實現(xiàn),從具體的業(yè)務(wù)處理里面獨立出來,把它們實現(xiàn)成為單獨的算法類,從而形成一系列的算法,并讓這些算法可以相互替換。
策略模式的重心不是如何來實現(xiàn)算法,而是如何組織、調(diào)用這些算法,從而讓程序結(jié)構(gòu)更靈活、具有更好的維護性和擴展性。
- 策略模式和if-else語句
看了前面的示例,很多朋友會發(fā)現(xiàn),每個策略算法具體實現(xiàn)的功能,就是原來在if-else結(jié)構(gòu)中的具體實現(xiàn)。
沒錯,其實多個if-elseif語句表達的就是一個平等的功能結(jié)構(gòu),你要么執(zhí)行if,要不你就執(zhí)行else,或者是elseif,這個時候,if塊里面的實現(xiàn)和else塊里面的實現(xiàn)從運行地位上來講就是平等的。
而策略模式就是把各個平等的具體實現(xiàn)封裝到單獨的策略實現(xiàn)類了,然后通過上下文來與具體的策略類進行交互。
因此多個if-else語句可以考慮使用策略模式。
- 算法的平等性
策略模式一個很大的特點就是各個策略算法的平等性。對于一系列具體的策略算法,大家的地位是完全一樣的,正是因為這個平等性,才能實現(xiàn)算法之間可以相互替換。
所有的策略算法在實現(xiàn)上也是相互獨立的,相互之間是沒有依賴的。
所以可以這樣描述這一系列策略算法:策略算法是相同行為的不同實現(xiàn)。
- 誰來選擇具體的策略算法
在策略模式中,可以在兩個地方來進行具體策略的選擇。
一個是在客戶端,在使用上下文的時候,由客戶端來選擇具體的策略算法,然后把這個策略算法設(shè)置給上下文。前面的示例就是這種情況。
還有一個是客戶端不管,由上下文來選擇具體的策略算法,這個在后面講容錯恢復的時候給大家演示一下。
- Strategy的實現(xiàn)方式
在前面的示例中,Strategy都是使用的接口來定義的,這也是常見的實現(xiàn)方式。但是如果多個算法具有公共功能的話,可以把Strategy實現(xiàn)成為抽象類,然后把多個算法的公共功能實現(xiàn)到Strategy里面。
- 運行時策略的唯一性
運行期間,策略模式在每一個時刻只能使用一個具體的策略實現(xiàn)對象,雖然可以動態(tài)的在不同的策略實現(xiàn)中切換,但是同時只能使用一個。
- 增加新的策略
在前面的示例里面,體會到了策略模式中切換算法的方便,但是增加一個新的算法會怎樣呢?比如現(xiàn)在要實現(xiàn)如下的功能:對于公司的“戰(zhàn)略合作客戶”,統(tǒng)一8折。
其實很簡單,策略模式可以讓你很靈活的擴展新的算法。具體的做法是:先寫一個策略算法類來實現(xiàn)新的要求,然后在客戶端使用的時候指定使用新的策略算法類就可以了。
還是通過示例來說明。先添加一個實現(xiàn)要求的策略類,示例代碼如下:
/**
* 具體算法實現(xiàn),為戰(zhàn)略合作客戶客戶計算應(yīng)報的價格
*/
public class CooperateCustomerStrategy implements Strategy{
public double calcPrice(double goodsPrice) {
System.out.println("對于戰(zhàn)略合作客戶,統(tǒng)一8折");
return goodsPrice*0.8;
}
}
然后在客戶端指定使用策略的時候指定新的策略算法實現(xiàn),示例如下:
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);
}
}
- 策略模式調(diào)用順序示意圖
策略模式的調(diào)用順序,有兩種常見的情況,一種如同前面的示例,具體如下:
先是客戶端來選擇并創(chuàng)建具體的策略對象
然后客戶端創(chuàng)建上下文
接下來客戶端就可以調(diào)用上下文的方法來執(zhí)行功能了,在調(diào)用的時候,從客戶端傳入算法需要的參數(shù)
上下文接到客戶的調(diào)用請求,會把這個請求轉(zhuǎn)發(fā)給它持有的Strategy
這種情況的調(diào)用順序示意圖如圖所示:

策略模式調(diào)用還有一種情況,就是把Context當做參數(shù)來傳遞給Strategy,這種方式的調(diào)用順序圖,在講具體的Context和Strategy的關(guān)系時再給出。
3.2 容錯恢復機制##
容錯恢復機制是應(yīng)用程序開發(fā)中非常常見的功能。那么什么是容錯恢復呢?簡單點說就是:程序運行的時候,正常情況下應(yīng)該按照某種方式來做,如果按照某種方式來做發(fā)生錯誤的話,系統(tǒng)并不會崩潰,也不會就此不能繼續(xù)向下運行了,而是有容忍出錯的能力,不但能容忍程序運行出現(xiàn)錯誤,還提供出現(xiàn)錯誤后的備用方案,也就是恢復機制,來代替正常執(zhí)行的功能,使程序繼續(xù)向下運行。
舉個實際點的例子吧,比如在一個系統(tǒng)中,所有對系統(tǒng)的操作都要有日志記錄,而且這個日志還需要有管理界面,這種情況下通常會把日志記錄在數(shù)據(jù)庫里面,方便后續(xù)的管理,但是在記錄日志到數(shù)據(jù)庫的時候,可能會發(fā)生錯誤,比如暫時連不上數(shù)據(jù)庫了,那就先記錄在文件里面,然后在合適的時候把文件中的記錄再轉(zhuǎn)錄到數(shù)據(jù)庫中。
對于這樣的功能的設(shè)計,就可以采用策略模式,把日志記錄到數(shù)據(jù)庫和日志記錄到文件當作兩種記錄日志的策略,然后在運行期間根據(jù)需要進行動態(tài)的切換。
在這個例子的實現(xiàn)中,要示范由上下文來選擇具體的策略算法,前面的例子都是由客戶端選擇好具體的算法,然后設(shè)置到上下文中。
- 先定義日志策略接口,很簡單,就是一個記錄日志的方法,示例代碼如下:
/**
* 日志記錄策略的接口
*/
public interface LogStrategy {
/**
* 記錄日志
* @param msg 需記錄的日志信息
*/
public void log(String msg);
}
- 實現(xiàn)日志策略接口,先實現(xiàn)默認的數(shù)據(jù)庫實現(xiàn),假設(shè)如果日志的長度超過長度就出錯,制造錯誤的是一個最常見的運行期錯誤,示例代碼如下:
/**
* 把日志記錄到數(shù)據(jù)庫
*/
public class DbLog implements LogStrategy{
public void log(String msg) {
//制造錯誤
if(msg!=null && msg.trim().length()>5){
int a = 5/0;
}
System.out.println("現(xiàn)在把 '"+msg+"' 記錄到數(shù)據(jù)庫中");
}
}
接下來實現(xiàn)記錄日志到文件中去,示例代碼如下:
/**
* 把日志記錄到文件
*/
public class FileLog implements LogStrategy{
public void log(String msg) {
System.out.println("現(xiàn)在把 '"+msg+"' 記錄到文件中");
}
}
- 接下來定義使用這些策略的上下文,注意這次是在上下文里面實現(xiàn)具體策略算法的選擇,所以不需要客戶端來指定具體的策略算法了,示例代碼如下:

- 看看現(xiàn)在的客戶端,沒有了選擇具體實現(xiàn)策略算法的工作,變得非常簡單,故意多調(diào)用一次,可以看出不同的效果,示例代碼如下:

- 小結(jié)一下,通過上面的示例,會看到策略模式的一種簡單應(yīng)用,也順便了解一下基本的容錯恢復機制的設(shè)計和實現(xiàn)。在實際的應(yīng)用中,需要設(shè)計容錯恢復的系統(tǒng)一般要求都比較高,應(yīng)用也會比較復雜,但是基本的思路是差不多的。
3.3 Context和Strategy的關(guān)系##
在策略模式中,通常是上下文使用具體的策略實現(xiàn)對象,反過來,策略實現(xiàn)對象也可以從上下文獲取所需要的數(shù)據(jù),因此可以將上下文當參數(shù)傳遞給策略實現(xiàn)對象,這種情況下上下文和策略實現(xiàn)對象是緊密耦合的。
在這種情況下,上下文封裝著具體策略對象進行算法運算所需要的數(shù)據(jù),具體策略對象通過回調(diào)上下文的方法來獲取這些數(shù)據(jù)。
甚至在某些情況下,策略實現(xiàn)對象還可以回調(diào)上下文的方法來實現(xiàn)一定的功能,這種使用場景下,上下文變相充當了多個策略算法實現(xiàn)的公共接口,在上下文定義的方法可以當做是所有或者是部分策略算法使用的公共功能。
但是請注意,由于所有的策略實現(xiàn)對象都實現(xiàn)同一個策略接口,傳入同一個上下文,可能會造成傳入的上下文數(shù)據(jù)的浪費,因為有的算法會使用這些數(shù)據(jù),而有的算法不會使用,但是上下文和策略對象之間交互的開銷是存在的了。
還是通過例子來說明。
- 工資支付的實現(xiàn)思路
考慮這樣一個功能:工資支付方式的問題,很多企業(yè)的工資支付方式是很靈活的,可支付方式是比較多的,比如:人民幣現(xiàn)金支付、美元現(xiàn)金支付、銀行轉(zhuǎn)賬到工資帳戶、銀行轉(zhuǎn)賬到工資卡;一些創(chuàng)業(yè)型的企業(yè)為了留住骨干員工,還可能有:工資轉(zhuǎn)股權(quán)等等方式??傊痪湓?,工資支付方式很多。
隨著公司的發(fā)展,會不斷有新的工資支付方式出現(xiàn),這就要求能方便的擴展;另外工資支付方式不是固定的,是由公司和員工協(xié)商確定的,也就是說可能不同的員工采用的是不同的支付方式,甚至同一個員工,不同時間采用的支付方式也可能會不同,這就要求能很方便的切換具體的支付方式。
要實現(xiàn)這樣的功能,策略模式是一個很好的選擇。在實現(xiàn)這個功能的時候,不同的策略算法需要的數(shù)據(jù)是不一樣,比如:現(xiàn)金支付就不需要銀行帳號,而銀行轉(zhuǎn)賬就需要帳號。這就導致在設(shè)計策略接口中的方法時,不太好確定參數(shù)的個數(shù),而且,就算現(xiàn)在把所有的參數(shù)都列上了,今后擴展呢?難道再來修改策略接口嗎?如果這樣做,那無異于一場災(zāi)難,加入一個新策略,就需要修改接口,然后修改所有已有的實現(xiàn),不瘋掉才怪!那么到底如何實現(xiàn),在今后擴展的時候才最方便呢?
解決方案之一,就是把上下文當做參數(shù)傳遞給策略對象,這樣一來,如果要擴展新的策略實現(xiàn),只需要擴展上下文就可以了,已有的實現(xiàn)不需要做任何的修改。
這樣是不是能很好的實現(xiàn)功能,并具有很好的擴展性呢?還是通過代碼示例來具體的看。假設(shè)先實現(xiàn)人民幣現(xiàn)金支付和美元現(xiàn)金支付這兩種支付方式,然后就進行使用測試,然后再來添加銀行轉(zhuǎn)賬到工資卡的支付方式,看看是不是能很容易的與已有的實現(xiàn)結(jié)合上。
- 實現(xiàn)代碼示例
(1)先定義工資支付的策略接口,就是定義一個支付工資的方法,示例代碼如下:
/**
* 支付工資的策略的接口,公司有多種支付工資的算法
* 比如:現(xiàn)金、銀行卡、現(xiàn)金加股票、現(xiàn)金加期權(quán)、美元支付等等
*/
public interface PaymentStrategy {
/**
* 公司給某人真正支付工資
* @param ctx 支付工資的上下文,里面包含算法需要的數(shù)據(jù)
*/
public void pay(PaymentContext ctx);
}
(2)定義好了工資支付的策略接口,該來考慮如何實現(xiàn)這多種支付策略了。
為了演示的簡單,這里先簡單實現(xiàn)人民幣現(xiàn)金支付和美元現(xiàn)金支付方式,當然并不真的去實現(xiàn)跟銀行的交互,只是示意一下。
人民幣現(xiàn)金支付的策略實現(xiàn),示例代碼如下:
/**
* 人民幣現(xiàn)金支付
*/
public class RMBCash implements PaymentStrategy{
public void pay(PaymentContext ctx) {
System.out.println("現(xiàn)在給"+ctx.getUserName()+"人民幣現(xiàn)金支付"+ctx.getMoney()+"元");
}
}
同樣的實現(xiàn)美元現(xiàn)金支付的策略,示例代碼如下:
/**
* 美元現(xiàn)金支付
*/
public class DollarCash implements PaymentStrategy{
public void pay(PaymentContext ctx) {
System.out.println("現(xiàn)在給"+ctx.getUserName()+"美元現(xiàn)金支付"+ctx.getMoney()+"元");
}
}
(3)該來看支付上下文的實現(xiàn)了,當然這個使用支付策略的上下文,是需要知道具體使用哪一個支付策略的,一般由客戶端來確定具體使用哪一個具體的策略,然后上下文負責去真正執(zhí)行。因此,這個上下文需要持有一個支付策略,而且是由客戶端來配置它。示例代碼如下:
/**
* 支付工資的上下文,每個人的工資不同,支付方式也不同
*/
public class PaymentContext {
/**
* 應(yīng)被支付工資的人員,簡單點,用姓名來代替
*/
private String userName = null;
/**
* 應(yīng)被支付的工資的金額
*/
private double money = 0.0;
/**
* 支付工資的方式策略的接口
*/
private PaymentStrategy strategy = null;
/**
* 構(gòu)造方法,傳入被支付工資的人員,應(yīng)支付的金額和具體的支付策略
* @param userName 被支付工資的人員
* @param money 應(yīng)支付的金額
* @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)建相應(yī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();
}
}
- 擴展示例,實現(xiàn)方式一
經(jīng)過上面的測試可以看出,通過使用策略模式,已經(jīng)實現(xiàn)好了兩種支付方式了。如果現(xiàn)在要增加一種支付方式,要求能支付到銀行卡,該怎么擴展最簡單呢?
應(yīng)該新增加一種支付到銀行卡的策略實現(xiàn),然后通過繼承來擴展支付上下文,在里面添加新的支付方式需要的新的數(shù)據(jù),比如銀行卡賬戶,然后在客戶端使用新的上下文和新的策略實現(xiàn)就可以了,這樣已有的實現(xiàn)都不需要改變,完全遵循開-閉原則。
先看看擴展的支付上下文對象的實現(xiàn),示例代碼如下:
/**
* 擴展的支付上下文對象
*/
public class PaymentContext2 extends PaymentContext {
/**
* 銀行帳號
*/
private String account = null;
/**
* 構(gòu)造方法,傳入被支付工資的人員,應(yīng)支付的金額和具體的支付策略
* @param userName 被支付工資的人員
* @param money 應(yīng)支付的金額
* @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;
}
}
然后看看新的策略算法的實現(xiàn),示例代碼如下:
/**
* 支付到銀行卡
*/
public class Card implements PaymentStrategy{
public void pay(PaymentContext ctx) {
// 這個新的算法自己知道要使用擴展的支付上下文,所以強制造型一下
PaymentContext2 ctx2 = (PaymentContext2)ctx;
System.out.println(" 現(xiàn)在給 "+ctx2.getUserName()+" 的 "+ctx2.getAccount()+" 帳號支付了 "+ctx2.getMoney()+" 元 ");
// 連接銀行,進行轉(zhuǎn)帳,就不去管了
}
}
最后看看客戶端怎么使用這個新的策略呢?原有的代碼不變,直接添加新的測試就可以了,示例代碼如下:
public class Client {
public static void main(String[] args) {
//創(chuàng)建相應(yī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();
}
}
- 擴展示例,實現(xiàn)方式二
(1)上面這種實現(xiàn)方式,是通過擴展上下文對象來準備新的算法需要的數(shù)據(jù)。還有另外一種方式,那就是通過策略的構(gòu)造方法來傳入新算法需要的數(shù)據(jù)。這樣實現(xiàn)的話,就不需要擴展上下文了,直接添加新的策略算法實現(xiàn)就好了。示例代碼如下:
/**
* 支付到銀行卡
*/
public class Card2 implements PaymentStrategy{
/**
* 帳號信息
*/
private String account = "";
/**
* 構(gòu)造方法,傳入帳號信息
* @param account 帳號信息
*/
public Card2(String account){
this.account = account;
}
public void pay(PaymentContext ctx) {
System.out.println(" 現(xiàn)在給 "+ctx.getUserName()+" 的 "+this.account+" 帳號支付了 "+ctx.getMoney()+" 元 ");
// 連接銀行,進行轉(zhuǎn)帳,就不去管了
}
}
(2)直接在客戶端測試就可以了,測試示例代碼如下:
public class Client {
public static void main(String[] args) {
//測試新添加的支付方式
PaymentStrategy strategyCard2 = new Card2("010998877656");
PaymentContext ctx4 = new PaymentContext("小張",9000,strategyCard2);
ctx4.payNow();
}
}
(3)現(xiàn)在有這么兩種擴展的實現(xiàn)方式,到底使用哪一種呢?或者是哪種實現(xiàn)更好呢?下面來比較一下:
對于擴展上下文的方式:這樣實現(xiàn),所有策略的實現(xiàn)風格更統(tǒng)一,策略需要的數(shù)據(jù)都統(tǒng)一從上下文來獲取,這樣在使用方法上也很統(tǒng)一;另外,在上下文中添加新的數(shù)據(jù),別的相應(yīng)算法也可以用得上,可以視為公共的數(shù)據(jù)。但缺點也很明顯,如果這些數(shù)據(jù)只有一個特定的算法來使用,那么這些數(shù)據(jù)有些浪費;另外每次添加新的算法都去擴展上下文,容易形成復雜的上下文對象層次,也未見得有必要。
對于在策略算法的實現(xiàn)上添加自己需要的數(shù)據(jù)的方式:這樣實現(xiàn),比較好想,實現(xiàn)簡單。但是缺點也很明顯,跟其它策略實現(xiàn)的風格不一致,其它策略都是從上下文中來獲取數(shù)據(jù),而這個策略的實現(xiàn)一部分數(shù)據(jù)來自上下文,一部分數(shù)據(jù)來自自己,有些不統(tǒng)一;另外,這樣一來,外部使用這些策略算法的時候也不一樣了,不太好以一個統(tǒng)一的方式來動態(tài)切換策略算法。
兩種實現(xiàn)各有優(yōu)劣,至于如何選擇,那就具體問題,具體的分析了。
- 另一種策略模式調(diào)用順序示意圖
策略模式調(diào)用還有一種情況,就是把Context當做參數(shù)來傳遞給Strategy,也就是本例示范的這種方式,這個時候策略模式的調(diào)用順序如圖所示:

3.4 策略模式結(jié)合模板方法模式##
在實際應(yīng)用策略模式的過程中,經(jīng)常會出現(xiàn)這樣一種情況,就是發(fā)現(xiàn)這一系列算法的實現(xiàn)上存在公共功能,甚至這一系列算法的實現(xiàn)步驟都是一樣的,只是在某些局部步驟上有所不同,這個時候,就需要對策略模式進行些許的變化使用了。
對于一系列算法的實現(xiàn)上存在公共功能的情況,策略模式可以有如下三種實現(xiàn)方式:
一個是在上下文當中實現(xiàn)公共功能,讓所有具體的策略算法回調(diào)這些方法。
另外一種情況就是把策略的接口改成抽象類,然后在里面實現(xiàn)具體算法的公共功能。
還有一種情況是給所有的策略算法定義一個抽象的父類,讓這個父類去實現(xiàn)策略的接口,然后在這個父類里面去實現(xiàn)公共的功能。
更進一步,如果這個時候發(fā)現(xiàn)“一系列算法的實現(xiàn)步驟都是一樣的,只是在某些局部步驟上有所不同”的情況,那就可以在這個抽象類里面定義算法實現(xiàn)的骨架,然后讓具體的策略算法去實現(xiàn)變化的部分。這樣的一個結(jié)構(gòu)自然就變成了策略模式來結(jié)合模板方法模式了,那個抽象類就成了模板方法模式的模板類。
我們討論過模板方法模式來結(jié)合策略模式的方式,也就是主要的結(jié)構(gòu)是模板方法模式,局部采用策略模式。而這里討論的是策略模式來結(jié)合模板方法模式,也就是主要的結(jié)構(gòu)是策略模式,局部實現(xiàn)上采用模板方法模式。通過這個示例也可以看出來,模式之間的結(jié)合是沒有定勢的,要具體問題具體分析。
此時策略模式結(jié)合模板方法模式的系統(tǒng)結(jié)構(gòu)如下圖所示:

還是用實際的例子來說吧,比如上面那個記錄日志的例子,如果現(xiàn)在需要在所有的消息前面都添加上日志時間,也就是說現(xiàn)在記錄日志的步驟變成了:第一步為日志消息添加日志時間;第二步具體記錄日志。
那么該怎么實現(xiàn)呢?
- 記錄日志的策略接口沒有變化,為了看起來方便,還是示例一下,示例代碼如下:
/**
* 日志記錄策略的接口
*/
public interface LogStrategy {
/**
* 記錄日志
* @param msg 需記錄的日志信息
*/
public void log(String msg);
}
- 增加一個實現(xiàn)這個策略接口的抽象類,在里面定義記錄日志的算法骨架,相當于模板方法模式的模板,示例代碼如下:
/**
* 實現(xiàn)日志策略的抽象模板,實現(xiàn)給消息添加時間
*/
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())+" 內(nèi)容是:"+ msg;
//第二步:真正執(zhí)行日志記錄
doLog(msg);
}
/**
* 真正執(zhí)行日志記錄,讓子類去具體實現(xiàn)
* @param msg 需記錄的日志信息
*/
protected abstract void doLog(String msg);
}
- 這個時候那兩個具體的日志算法實現(xiàn)也需要做些改變,不再直接實現(xiàn)策略接口了,而是繼承模板,實現(xiàn)模板方法了。這個時候記錄日志到數(shù)據(jù)庫的類,示例代碼如下:
/**
* 把日志記錄到數(shù)據(jù)庫
*/
public class DbLog extends LogStrategyTemplate{
//除了定義上發(fā)生了改變外,具體的實現(xiàn)沒變
public void doLog(String msg) {
//制造錯誤
if(msg!=null && msg.trim().length()>5){
int a = 5/0;
}
System.out.println("現(xiàn)在把 '"+msg+"' 記錄到數(shù)據(jù)庫中");
}
}
同理實現(xiàn)記錄日志到文件的類如下:
/**
* 把日志記錄到數(shù)據(jù)庫
*/
public class FileLog extends LogStrategyTemplate{
public void doLog(String msg) {
System.out.println("現(xiàn)在把 '"+msg+"' 記錄到文件中");
}
}
- 算法實現(xiàn)的改變不影響使用算法的上下文,上下文跟前面一樣,示例代碼如下:
/**
* 日志記錄的上下文
*/
public class LogContext {
/**
* 記錄日志的方法,提供給客戶端使用
* @param msg 需記錄的日志信息
*/
public void log(String msg){
//在上下文里面,自行實現(xiàn)對具體策略的選擇
//優(yōu)先選用策略:記錄到數(shù)據(jù)庫
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)缺點##
- 定義一系列算法
策略模式的功能就是定義一系列算法,實現(xiàn)讓這些算法可以相互替換。所以會為這一系列算法定義公共的接口,以約束一系列算法要實現(xiàn)的功能。如果這一系列算法具有公共功能,可以把策略接口實現(xiàn)成為抽象類,把這些公共功能實現(xiàn)到父類里面,對于這個問題,前面講了三種處理方法,這里就不羅嗦了。
- 避免多重條件語句
根據(jù)前面的示例會發(fā)現(xiàn),策略模式的一系列策略算法是平等的,可以互換的,寫在一起就是通過if-else結(jié)構(gòu)來組織,如果此時具體的算法實現(xiàn)里面又有條件語句,就構(gòu)成了多重條件語句,使用策略模式能避免這樣的多重條件語句。
- 更好的擴展性
在策略模式中擴展新的策略實現(xiàn)非常容易,只要增加新的策略實現(xiàn)類,然后在選擇使用策略的地方選擇使用這個新的策略實現(xiàn)就好了。
- 客戶必須了解每種策略的不同
策略模式也有缺點,比如讓客戶端來選擇具體使用哪一個策略,這就可能會讓客戶需要了解所有的策略,還要了解各種策略的功能和不同,這樣才能做出正確的選擇,而且這樣也暴露了策略的具體實現(xiàn)。
- 增加了對象數(shù)目
由于策略模式把每個具體的策略實現(xiàn)都單獨封裝成為類,如果備選的策略很多的話,那么對象的數(shù)目就會很可觀。
- 只適合扁平的算法結(jié)構(gòu)
策略模式的一系列算法地位是平等的,是可以相互替換的,事實上構(gòu)成了一個扁平的算法結(jié)構(gòu),也就是在一個策略接口下,有多個平等的策略算法,就相當于兄弟算法。而且在運行時刻只有一個算法被使用,這就限制了算法使用的層級,使用的時候不能嵌套使用。
對于出現(xiàn)需要嵌套使用多個算法的情況,比如折上折、折后返卷等業(yè)務(wù)的實現(xiàn),需要組合或者是嵌套使用多個算法的情況,可以考慮使用裝飾模式、或是變形的職責鏈、或是AOP等方式來實現(xiàn)。
3.6 思考策略模式##
- 策略模式的本質(zhì)
策略模式的本質(zhì):分離算法,選擇實現(xiàn)。
仔細思考策略模式的結(jié)構(gòu)和實現(xiàn)的功能,會發(fā)現(xiàn),如果沒有上下文,策略模式就回到了最基本的接口和實現(xiàn)了,只要是面向接口編程的,那么就能夠享受到接口的封裝隔離帶來的好處。也就是通過一個統(tǒng)一的策略接口來封裝和隔離具體的策略算法,面向接口編程的話,自然不需要關(guān)心具體的策略實現(xiàn),也可以通過使用不同的實現(xiàn)類來實例化接口,從而實現(xiàn)切換具體的策略。
看起來好像沒有上下文什么事情,但是如果沒有上下文,那么就需要客戶端來直接與具體的策略交互,尤其是當需要提供一些公共功能,或者是相關(guān)狀態(tài)存儲的時候,會大大增加客戶端使用的難度。因此,引入上下文還是很必要的,有了上下文,這些工作就由上下文來完成了,客戶端只需要與上下文交互就可以了,這樣會讓整個設(shè)計模式更獨立、更有整體性,也讓客戶端更簡單。
但縱觀整個策略模式實現(xiàn)的功能和設(shè)計,它的本質(zhì)還是“分離算法,選擇實現(xiàn)”,因為分離并封裝了算法,才能夠很容易的修改和添加算法;也能很容易的動態(tài)切換使用不同的算法,也就是動態(tài)選擇一個算法來實現(xiàn)需要的功能了。
- 對設(shè)計原則的體現(xiàn)
從設(shè)計原則上來看,策略模式很好的體現(xiàn)了開-閉原則。策略模式通過把一系列可變的算法進行封裝,并定義出合理的使用結(jié)構(gòu),使得在系統(tǒng)出現(xiàn)新算法的時候,能很容易的把新的算法加入到已有的系統(tǒng)中,而已有的實現(xiàn)不需要做任何修改。這在前面的示例中已經(jīng)體現(xiàn)出來了,好好體會一下。
從設(shè)計原則上來看,策略模式還很好的體現(xiàn)了里氏替換原則。策略模式是一個扁平結(jié)構(gòu),一系列的實現(xiàn)算法其實是兄弟關(guān)系,都是實現(xiàn)同一個接口或者繼承的同一個父類。這樣只要使用策略的客戶保持面向抽象類型編程,就能夠使用不同的策略的具體實現(xiàn)對象來配置它,從而實現(xiàn)一系列算法可以相互替換。
- 何時選用策略模式
建議在如下情況中,選用策略模式:
出現(xiàn)有許多相關(guān)的類,僅僅是行為有差別的情況,可以使用策略模式來使用多個行為中的一個來配置一個類的方法,實現(xiàn)算法動態(tài)切換
出現(xiàn)同一個算法,有很多不同的實現(xiàn)的情況,可以使用策略模式來把這些“不同的實現(xiàn)”實現(xiàn)成為一個算法的類層次
需要封裝算法中,與算法相關(guān)的數(shù)據(jù)的情況,可以使用策略模式來避免暴露這些跟算法相關(guān)的數(shù)據(jù)結(jié)構(gòu)
出現(xiàn)抽象一個定義了很多行為的類,并且是通過多個if-else語句來選擇這些行為的情況,可以使用策略模式來代替這些條件語句
3.7 相關(guān)模式##
- 策略模式和狀態(tài)模式
這兩個模式從模式結(jié)構(gòu)上看是一樣的,但是實現(xiàn)的功能是不一樣的。
狀態(tài)模式是根據(jù)狀態(tài)的變化來選擇相應(yīng)的行為,不同的狀態(tài)對應(yīng)不同的類,每個狀態(tài)對應(yīng)的類實現(xiàn)了該狀態(tài)對應(yīng)的功能,在實現(xiàn)功能的同時,還會維護狀態(tài)數(shù)據(jù)的變化。這些實現(xiàn)狀態(tài)對應(yīng)的功能的類之間是不能相互替換的。
策略模式是根據(jù)需要或者是客戶端的要求來選擇相應(yīng)的實現(xiàn)類,各個實現(xiàn)類是平等的,是可以相互替換的。
另外策略模式可以讓客戶端來選擇需要使用的策略算法,而狀態(tài)模式一般是由上下文,或者是在狀態(tài)實現(xiàn)類里面來維護具體的狀態(tài)數(shù)據(jù),通常不由客戶端來指定狀態(tài)。
- 策略模式和模板方法模式
這兩個模式可組合使用,如同前面示例的那樣。
模板方法重在封裝算法骨架,而策略模式重在分離并封裝算法實現(xiàn)。
- 策略模式和享元模式
這兩個模式可組合使用。
策略模式分離并封裝出一系列的策略算法對象,這些對象的功能通常都比較單一,很多時候就是為了實現(xiàn)某個算法的功能而存在,因此,針對這一系列的、多個細粒度的對象,可以應(yīng)用享元模式來節(jié)省資源,但前提是這些算法對象要被頻繁的使用,如果偶爾用一次,就沒有必要做成享元了。