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

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

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

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

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

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

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

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