| 先感謝眾多朋友的支持、評論和鼓勵,只有多多努力,寫點好的博文來回報大家的好意!? 接下來想寫寫另外一個雖然較簡單,但是使用很頻繁的模式——策略模式 策略模式(Strategy) 1? 場景問題 1.1? 報價管理 向客戶報價,對于銷售部門的人來講,這是一個非常重大、非常復雜的問題,對不同的客戶要報不同的價格,比如: (1)對普通客戶或者是新客戶報的是全價 (2)對老客戶報的價格,根據客戶年限,給予一定的折扣 (3)對大客戶報的價格,根據大客戶的累計消費金額,給予一定的折扣 (4)還要考慮客戶購買的數量和金額,比如:雖然是新用戶,但是一次購買的數量非常大,或者是總金額非常高,也會有一定的折扣 (5)還有,報價人員的職務高低,也決定了他是否有權限對價格進行一定的浮動折扣 甚至在不同的階段,對客戶的報價也不同,一般情況是剛開始比較高,越接近成交階段,報價越趨于合理。 總之,向客戶報價是非常復雜的,因此在一些CRM(客戶關系管理)的系統中,會有一個單獨的報價管理模塊,來處理復雜的報價功能。 為了演示的簡潔性,假定現在需要實現一個簡化的報價管理,實現如下的功能: (1)對普通客戶或者是新客戶報全價 (2)對老客戶報的價格,統一折扣5% (3)對大客戶報的價格,統一折扣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("對于老客戶,統一折扣5%"); ?????????? return goodsPrice*(1-0.05); ?????? }else if(customerType.equals("大客戶")){ ?????????? System.out.println("對于大客戶,統一折扣10%"); ?????????? return goodsPrice*(1-0.1);???????? ?????? } ?????? //其余人員都是報原價 ?????? return goodsPrice; ??? } } |
1.3? 有何問題 上面的寫法是很簡單的,也很容易想,但是仔細想想,這樣實現,問題可不小,比如: (1)第一個問題:價格類包含了所有計算報價的算法,使得價格類,尤其是報價這個方法比較龐雜,難以維護。 有朋友可能會想,這很簡單嘛,把這些算法從報價方法里面拿出去,形成獨立的方法不就可以解決這個問題了嗎?據此寫出如下的實現代碼,示例代碼如下:? | /** ?* 價格管理,主要完成計算向客戶所報價格的功能 ?*/ 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("對于老客戶,統一折扣5%"); ?????? return goodsPrice*(1-0.05); ??? } ??? /** ??? ?* 為大客戶計算應報的價格 ??? ?* @param goodsPrice 商品銷售原價 ??? ?* @return 計算出來的,應該給客戶報的價格 ??? ?*/ ??? private double calcPriceForLarge(double goodsPrice){ ?????? System.out.println("對于大客戶,統一折扣10%"); ?????? return goodsPrice*(1-0.1);? ??? } } |
這樣看起來,比剛開始稍稍好點,計算報價的方法會稍稍簡單一點,這樣維護起來也稍好一些,某個算法發生了變化,直接修改相應的私有方法就可以了。擴展起來也容易一點,比如要增加一個“戰略合作客戶”的類型,報價為直接8折,就只需要在價格類里面新增加一個私有的方法來計算新的價格,然后在計算報價的方法里面新添一個else-if即可。看起來似乎很不錯了。 真的很不錯了嗎? 再想想,問題還是存在,只不過從計算報價的方法挪動到價格類里面了,假如有100個或者更多這樣的計算方式,這會讓這個價格類非常龐大,難以維護。而且,維護和擴展都需要去修改已有的代碼,這是很不好的,違反了開-閉原則。? (2)第二個問題:經常會有這樣的需要,在不同的時候,要使用不同的計算方式。 比如:在公司周年慶的時候,所有的客戶額外增加3%的折扣;在換季促銷的時候,普通客戶是額外增加折扣2%,老客戶是額外增加折扣3%,大客戶是額外增加折扣5%。這意味著計算報價的方式會經常被修改,或者被切換。 通常情況下應該是被切換,因為過了促銷時間,又還回到正常的價格體系上來了。而現在的價格類中計算報價的方法,是固定調用各種計算方式,這使得切換調用不同的計算方式很麻煩,每次都需要修改if-else里面的調用代碼。 看到這里,可能有朋友會想,那么到底應該如何實現,才能夠讓價格類中的計算報價的算法,能很容易的實現可維護、可擴展,又能動態的切換變化呢? 2? 解決方案 2.1? 策略模式來解決 用來解決上述問題的一個合理的解決方案就是策略模式。那么什么是策略模式呢? (1)策略模式定義 定義一系列的算法,把它們一個個封裝起來,并且使它們可相互替換。本模式使得算法可獨立于使用它的客戶而變化。 (2)應用策略模式來解決的思路 仔細分析上面的問題,先來把它抽象一下,各種計算報價的計算方式就好比是具體的算法,而使用這些計算方式來計算報價的程序,就相當于是使用算法的客戶。 再分析上面的實現方式,為什么會造成那些問題,根本原因,就在于算法和使用算法的客戶是耦合的,甚至是密不可分的,在上面實現中,具體的算法和使用算法的客戶是同一個類里面的不同方法。 現在要解決那些問題,按照策略模式的方式,應該先把所有的計算方式獨立出來,每個計算方式做成一個單獨的算法類,從而形成一系列的算法,并且為這一系列算法定義一個公共的接口,這些算法實現是同一接口的不同實現,地位是平等的,可以相互替換。這樣一來,要擴展新的算法就變成了增加一個新的算法實現類,要維護某個算法,也只是修改某個具體的算法實現即可,不會對其它代碼造成影響。也就是說這樣就解決了可維護、可擴展的問題。 為了實現讓算法能獨立于使用它的客戶,策略模式引入了一個上下文的對象,這個對象負責持有算法,但是不負責決定具體選用哪個算法,把選擇算法的功能交給了客戶,由客戶選擇好具體的算法后,設置到上下文對象里面,讓上下文對象持有客戶選擇的算法,當客戶通知上下文對象執行功能的時候,上下文對象會去轉調具體的算法。這樣一來,具體的算法和直接使用算法的客戶是分離的。 具體的算法和使用它的客戶分離過后,使得算法可獨立于使用它的客戶而變化,并且能夠動態的切換需要使用的算法,只要客戶端動態的選擇使用不同的算法,然后設置到上下文對象中去,實際調用的時候,就可以調用到不同的算法。 2.2? 模式結構和說明 策略模式的結構示意圖如圖1所示:
? 圖1? 策略模式結構示意圖 Strategy: 策略接口,用來約束一系列具體的策略算法。Context使用這個接口來調用具體的策略實現定義的算法。 ConcreteStrategy: 具體的策略實現,也就是具體的算法實現。 Context: 上下文,負責和具體的策略類交互,通常上下文會持有一個真正的策略實現,上下文還可以讓具體的策略類來獲取上下文的數據,甚至讓具體的策略類來回調上下文的方法。 2.3? 策略模式示例代碼 (1)首先來看策略,也就是定義算法的接口,示例代碼如下:? | /** ?* 策略,定義算法的接口 ?*/ public interface Strategy { ??? /** ??? ?* 某個算法的接口,可以有傳入參數,也可以有返回值 ??? ?*/ ??? public void algorithmInterface(); } |
(2)該來看看具體的算法實現了,定義了三個,分別是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() { ?????? //具體的算法實現??? ??? } } |
(3)再來看看上下文的實現,示例代碼如下:? | /** ?* 上下文對象,通常會持有一個具體的策略對象 ?*/ public class Context { ??? /** ??? ?* 持有一個具體的策略對象 ??? ?*/ ??? private Strategy strategy; ??? /** ??? ?* 構造方法,傳入一個具體的策略對象 ??? ?* @param aStrategy 具體的策略對象 ??? ?*/ ??? public Context(Strategy aStrategy) { ?????? this.strategy = aStrategy; ??? } ??? /** ??? ?* 上下文對客戶端提供的操作接口,可以有參數和返回值 ??? ?*/ ??? public void contextInterface() { ?????? //通常會轉調具體的策略對象進行算法運算 ?????? strategy.algorithmInterface(); ??? } } |
?2.4? 使用策略模式重寫示例 要使用策略模式來重寫前面報價的示例,大致有如下改變: - 首先需要定義出算法的接口。
- 然后把各種報價的計算方式單獨出來,形成算法類。
- 對于Price這個類,把它當做上下文,在計算報價的時候,不再需要判斷,直接使用持有的具體算法進行運算即可。選擇使用哪一個算法的功能挪出去,放到外部使用的客戶端去。
這個時候,程序的結構如圖2所示: ?圖2? 使用策略模式實現示例的結構示意圖 (1)先看策略接口,示例代碼如下:? | /** ?* 策略,定義計算報價算法的接口 ?*/ public interface Strategy { ??? /** ??? ?* 計算應報的價格 ??? ?* @param goodsPrice 商品銷售原價 ??? ?* @return 計算出來的,應該給客戶報的價格 ??? ?*/ ??? public double calcPrice(double goodsPrice); } |
(2)接下來看看具體的算法實現,不同的算法,實現也不一樣,先看為新客戶或者是普通客戶計算應報的價格的實現,示例代碼如下:? | /** ?* 具體算法實現,為新客戶或者是普通客戶計算應報的價格 ?*/ 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("對于老客戶,統一折扣5%"); ?????? return goodsPrice*(1-0.05); ??? } } |
再看看為大客戶計算應報的價格的實現,示例代碼如下:?? | /** ?* 具體算法實現,為大客戶計算應報的價格 ?*/ public class LargeCustomerStrategy implements Strategy{ ??? public double calcPrice(double goodsPrice) { ?????? System.out.println("對于大客戶,統一折扣10%"); ?????? return goodsPrice*(1-0.1); ??? } } |
(3)接下來看看上下文的實現,也就是原來的價格類,它的變化比較大,主要有: - 原來那些私有的,用來做不同計算的方法,已經去掉了,獨立出去做成了算法類
- 原來報價方法里面,對具體計算方式的判斷,去掉了,讓客戶端來完成選擇具體算法的功能
- 新添加持有一個具體的算法實現,通過構造方法傳入
- 原來報價方法的實現,變化成了轉調具體算法來實現
示例代碼如下:? | /** ?* 價格管理,主要完成計算向客戶所報價格的功能 ?*/ 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); ??? } } |
(4)寫個客戶端來測試運行一下,好加深體會,示例代碼如下:?? | public class Client { ??? public static void main(String[] args) { ?????? //1:選擇并創建需要使用的策略對象 ?????? Strategy strategy = new LargeCustomerStrategy (); ?????? //2:創建上下文 ?????? Price ctx = new Price(strategy); ?????? //3:計算報價 ?????? double quote = ctx.quote(1000); ?????? System.out.println("向客戶報價:"+quote); ??? } } |
運行一下,看看效果。 你可以修改使用不同的策略算法具體實現,現在用的是LargeCustomerStrategy,你可以嘗試修改成其它兩種實現,試試看,體會一下切換算法的容易性。 3? 模式講解 3.1? 認識策略模式 (1)策略模式的功能 策略模式的功能是把具體的算法實現,從具體的業務處理里面獨立出來,把它們實現成為單獨的算法類,從而形成一系列的算法,并讓這些算法可以相互替換。 策略模式的重心不是如何來實現算法,而是如何組織、調用這些算法,從而讓程序結構更靈活、具有更好的維護性和擴展性。 (2)策略模式和if-else語句 看了前面的示例,很多朋友會發現,每個策略算法具體實現的功能,就是原來在if-else結構中的具體實現。 沒錯,其實多個if-elseif語句表達的就是一個平等的功能結構,你要么執行if,要不你就執行else,或者是elseif,這個時候,if塊里面的實現和else塊里面的實現從運行地位上來講就是平等的。 而策略模式就是把各個平等的具體實現封裝到單獨的策略實現類了,然后通過上下文來與具體的策略類進行交互。 因此多個if-else語句可以考慮使用策略模式。 (3)算法的平等性 策略模式一個很大的特點就是各個策略算法的平等性。對于一系列具體的策略算法,大家的地位是完全一樣的,正是因為這個平等性,才能實現算法之間可以相互替換。 所有的策略算法在實現上也是相互獨立的,相互之間是沒有依賴的。 所以可以這樣描述這一系列策略算法:策略算法是相同行為的不同實現。 (4)誰來選擇具體的策略算法 在策略模式中,可以在兩個地方來進行具體策略的選擇。 一個是在客戶端,在使用上下文的時候,由客戶端來選擇具體的策略算法,然后把這個策略算法設置給上下文。前面的示例就是這種情況。 還有一個是客戶端不管,由上下文來選擇具體的策略算法,這個在后面講容錯恢復的時候給大家演示一下。 (5)Strategy的實現方式 在前面的示例中,Strategy都是使用的接口來定義的,這也是常見的實現方式。但是如果多個算法具有公共功能的話,可以把Strategy實現成為抽象類,然后把多個算法的公共功能實現到Strategy里面。 (6)運行時策略的唯一性 運行期間,策略模式在每一個時刻只能使用一個具體的策略實現對象,雖然可以動態的在不同的策略實現中切換,但是同時只能使用一個。 (7)增加新的策略 在前面的示例里面,體會到了策略模式中切換算法的方便,但是增加一個新的算法會怎樣呢?比如現在要實現如下的功能:對于公司的“戰略合作客戶”,統一8折。 其實很簡單,策略模式可以讓你很靈活的擴展新的算法。具體的做法是:先寫一個策略算法類來實現新的要求,然后在客戶端使用的時候指定使用新的策略算法類就可以了。 還是通過示例來說明。先添加一個實現要求的策略類,示例代碼如下:?? | /** ?* 具體算法實現,為戰略合作客戶客戶計算應報的價格 ?*/ public class CooperateCustomerStrategy implements Strategy{ ??? public double calcPrice(double goodsPrice) { ?????? System.out.println("對于戰略合作客戶,統一8折"); ?????? return goodsPrice*0.8; ??? } } |
然后在客戶端指定使用策略的時候指定新的策略算法實現,示例如下:?? | public class Client2 { ??? public static void main(String[] args) { ?????? //1:選擇并創建需要使用的策略對象 ???????Strategy strategy = new CooperateCustomerStrategy (); ?????? //2:創建上下文 ?????? Price ctx = new Price(strategy); ?????? ?????? //3:計算報價 ?????? double quote = ctx.quote(1000); ?????? System.out.println("向客戶報價:"+quote); ??? } } |
除了加粗部分變動外,客戶端沒有其他的變化。? 運行客戶端,測試看看,好好體會一下。 除了客戶端發生變化外,已有的上下文、策略接口定義和策略的已有實現,都不需要做任何的修改,可見能很方便的擴展新的策略算法。 (8)策略模式調用順序示意圖 策略模式的調用順序,有兩種常見的情況,一種如同前面的示例,具體如下: a:先是客戶端來選擇并創建具體的策略對象 b:然后客戶端創建上下文 c:接下來客戶端就可以調用上下文的方法來執行功能了,在調用的時候,從客戶端傳入算法需要的參數 d:上下文接到客戶的調用請求,會把這個請求轉發給它持有的Strategy 這種情況的調用順序示意圖如圖3所示: ?圖3? 策略模式調用順序示意圖一 策略模式調用還有一種情況,就是把Context當做參數來傳遞給Strategy,這種方式的調用順序圖,在講具體的Context和Strategy的關系時再給出。 3.2? 容錯恢復機制 容錯恢復機制是應用程序開發中非常常見的功能。那么什么是容錯恢復呢?簡單點說就是:程序運行的時候,正常情況下應該按照某種方式來做,如果按照某種方式來做發生錯誤的話,系統并不會崩潰,也不會就此不能繼續向下運行了,而是有容忍出錯的能力,不但能容忍程序運行出現錯誤,還提供出現錯誤后的備用方案,也就是恢復機制,來代替正常執行的功能,使程序繼續向下運行。 舉個實際點的例子吧,比如在一個系統中,所有對系統的操作都要有日志記錄,而且這個日志還需要有管理界面,這種情況下通常會把日志記錄在數據庫里面,方便后續的管理,但是在記錄日志到數據庫的時候,可能會發生錯誤,比如暫時連不上數據庫了,那就先記錄在文件里面,然后在合適的時候把文件中的記錄再轉錄到數據庫中。 對于這樣的功能的設計,就可以采用策略模式,把日志記錄到數據庫和日志記錄到文件當作兩種記錄日志的策略,然后在運行期間根據需要進行動態的切換。 在這個例子的實現中,要示范由上下文來選擇具體的策略算法,前面的例子都是由客戶端選擇好具體的算法,然后設置到上下文中。 下面還是通過代碼來示例一下。 (1)先定義日志策略接口,很簡單,就是一個記錄日志的方法,示例代碼如下:?? | /** ?* 日志記錄策略的接口 ?*/ public interface LogStrategy { ??? /** ??? ?* 記錄日志 ??? ?* @param msg 需記錄的日志信息 ??? ?*/ ??? public void log(String msg); } |
(2)實現日志策略接口,先實現默認的數據庫實現,假設如果日志的長度超過長度就出錯,制造錯誤的是一個最常見的運行期錯誤,示例代碼如下:?? | /** ?* 把日志記錄到數據庫 ?*/ 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+"' 記錄到文件中"); ??? } } |
(3)接下來定義使用這些策略的上下文,注意這次是在上下文里面實現具體策略算法的選擇,所以不需要客戶端來指定具體的策略算法了,示例代碼如下: (4)看看現在的客戶端,沒有了選擇具體實現策略算法的工作,變得非常簡單,故意多調用一次,可以看出不同的效果,示例代碼如下: (5)小結一下,通過上面的示例,會看到策略模式的一種簡單應用,也順便了解一下基本的容錯恢復機制的設計和實現。在實際的應用中,需要設計容錯恢復的系統一般要求都比較高,應用也會比較復雜,但是基本的思路是差不多的。 3.3? Context和Strategy的關系 在策略模式中,通常是上下文使用具體的策略實現對象,反過來,策略實現對象也可以從上下文獲取所需要的數據,因此可以將上下文當參數傳遞給策略實現對象,這種情況下上下文和策略實現對象是緊密耦合的。 在這種情況下,上下文封裝著具體策略對象進行算法運算所需要的數據,具體策略對象通過回調上下文的方法來獲取這些數據。 甚至在某些情況下,策略實現對象還可以回調上下文的方法來實現一定的功能,這種使用場景下,上下文變相充當了多個策略算法實現的公共接口,在上下文定義的方法可以當做是所有或者是部分策略算法使用的公共功能。 但是請注意,由于所有的策略實現對象都實現同一個策略接口,傳入同一個上下文,可能會造成傳入的上下文數據的浪費,因為有的算法會使用這些數據,而有的算法不會使用,但是上下文和策略對象之間交互的開銷是存在的了。 還是通過例子來說明。 1:工資支付的實現思路 考慮這樣一個功能:工資支付方式的問題,很多企業的工資支付方式是很靈活的,可支付方式是比較多的,比如:人民幣現金支付、美元現金支付、銀行轉賬到工資帳戶、銀行轉賬到工資卡;一些創業型的企業為了留住骨干員工,還可能有:工資轉股權等等方式。總之一句話,工資支付方式很多。 隨著公司的發展,會不斷有新的工資支付方式出現,這就要求能方便的擴展;另外工資支付方式不是固定的,是由公司和員工協商確定的,也就是說可能不同的員工采用的是不同的支付方式,甚至同一個員工,不同時間采用的支付方式也可能會不同,這就要求能很方便的切換具體的支付方式。 要實現這樣的功能,策略模式是一個很好的選擇。在實現這個功能的時候,不同的策略算法需要的數據是不一樣,比如:現金支付就不需要銀行帳號,而銀行轉賬就需要帳號。這就導致在設計策略接口中的方法時,不太好確定參數的個數,而且,就算現在把所有的參數都列上了,今后擴展呢?難道再來修改策略接口嗎?如果這樣做,那無異于一場災難,加入一個新策略,就需要修改接口,然后修改所有已有的實現,不瘋掉才怪!那么到底如何實現,在今后擴展的時候才最方便呢? 解決方案之一,就是把上下文當做參數傳遞給策略對象,這樣一來,如果要擴展新的策略實現,只需要擴展上下文就可以了,已有的實現不需要做任何的修改。 這樣是不是能很好的實現功能,并具有很好的擴展性呢?還是通過代碼示例來具體的看。假設先實現人民幣現金支付和美元現金支付這兩種支付方式,然后就進行使用測試,然后再來添加銀行轉賬到工資卡的支付方式,看看是不是能很容易的與已有的實現結合上。 2:實現代碼示例 (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)該來看支付上下文的實現了,當然這個使用支付策略的上下文,是需要知道具體使用哪一個支付策略的,一般由客戶端來確定具體使用哪一個具體的策略,然后上下文負責去真正執行。因此,這個上下文需要持有一個支付策略,而且是由客戶端來配置它。示例代碼如下:? | /** ?* 支付工資的上下文,每個人的工資不同,支付方式也不同 ?*/ 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)準備好了支付工資的各種策略,下面看看如何使用這些策略來真正支付工資,很簡單,客戶端是使用上下文來使用具體的策略的,而且是客戶端來確定具體的策略,就是客戶端創建哪個策略,最終就運行哪一個策略,各個策略之間是可以動態切換的,示例代碼如下:? | public class Client { ??? public static void main(String[] args) { ?????? //創建相應的支付策略 ?????? 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(); ??? } } |
運行一下,看看效果,運行結果如下:?? | 現在給小李人民幣現金支付5000.0元 現在給Petter美元現金支付8000.0元 |
3:擴展示例,實現方式一 經過上面的測試可以看出,通過使用策略模式,已經實現好了兩種支付方式了。如果現在要增加一種支付方式,要求能支付到銀行卡,該怎么擴展最簡單呢? 應該新增加一種支付到銀行卡的策略實現,然后通過繼承來擴展支付上下文,在里面添加新的支付方式需要的新的數據,比如銀行卡賬戶,然后在客戶端使用新的上下文和新的策略實現就可以了,這樣已有的實現都不需要改變,完全遵循開-閉原則。 先看看擴展的支付上下文對象的實現,示例代碼如下:? | /** ?* 擴展的支付上下文對象 ?*/ 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) { ?????? //創建相應的支付策略 ?????? 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(); ??? } } |
再次測試,體會一下,運行結果如下:?? | 現在給小李人民幣現金支付5000.0元 現在給Petter美元現金支付8000.0元 現在給小王的010998877656帳號支付了9000.0元 |
4:擴展示例,實現方式二 同樣還是實現上面這個功能:現在要增加一種支付方式,要求能支付到銀行卡。 (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)現在有這么兩種擴展的實現方式,到底使用哪一種呢?或者是哪種實現更好呢?下面來比較一下: 對于擴展上下文的方式:這樣實現,所有策略的實現風格更統一,策略需要的數據都統一從上下文來獲取,這樣在使用方法上也很統一;另外,在上下文中添加新的數據,別的相應算法也可以用得上,可以視為公共的數據。但缺點也很明顯,如果這些數據只有一個特定的算法來使用,那么這些數據有些浪費;另外每次添加新的算法都去擴展上下文,容易形成復雜的上下文對象層次,也未見得有必要。 對于在策略算法的實現上添加自己需要的數據的方式:這樣實現,比較好想,實現簡單。但是缺點也很明顯,跟其它策略實現的風格不一致,其它策略都是從上下文中來獲取數據,而這個策略的實現一部分數據來自上下文,一部分數據來自自己,有些不統一;另外,這樣一來,外部使用這些策略算法的時候也不一樣了,不太好以一個統一的方式來動態切換策略算法。 兩種實現各有優劣,至于如何選擇,那就具體問題,具體的分析了。 5:另一種策略模式調用順序示意圖 策略模式調用還有一種情況,就是把Context當做參數來傳遞給Strategy,也就是本例示范的這種方式,這個時候策略模式的調用順序如圖4所示: ? 圖4? 策略模式調用順序示意圖二 3.4? 策略模式結合模板方法模式 在實際應用策略模式的過程中,經常會出現這樣一種情況,就是發現這一系列算法的實現上存在公共功能,甚至這一系列算法的實現步驟都是一樣的,只是在某些局部步驟上有所不同,這個時候,就需要對策略模式進行些許的變化使用了。 對于一系列算法的實現上存在公共功能的情況,策略模式可以有如下三種實現方式: - 一個是在上下文當中實現公共功能,讓所有具體的策略算法回調這些方法。
- 另外一種情況就是把策略的接口改成抽象類,然后在里面實現具體算法的公共功能。
- 還有一種情況是給所有的策略算法定義一個抽象的父類,讓這個父類去實現策略的接口,然后在這個父類里面去實現公共的功能。
更進一步,如果這個時候發現“一系列算法的實現步驟都是一樣的,只是在某些局部步驟上有所不同”的情況,那就可以在這個抽象類里面定義算法實現的骨架,然后讓具體的策略算法去實現變化的部分。這樣的一個結構自然就變成了策略模式來結合模板方法模式了,那個抽象類就成了模板方法模式的模板類。 在上一章我們討論過模板方法模式來結合策略模式的方式,也就是主要的結構是模板方法模式,局部采用策略模式。而這里討論的是策略模式來結合模板方法模式,也就是主要的結構是策略模式,局部實現上采用模板方法模式。通過這個示例也可以看出來,模式之間的結合是沒有定勢的,要具體問題具體分析。 此時策略模式結合模板方法模式的系統結構如下圖5所示:
圖5? 策略模式結合模板方法模式的結構示意圖 還是用實際的例子來說吧,比如上面那個記錄日志的例子,如果現在需要在所有的消息前面都添加上日志時間,也就是說現在記錄日志的步驟變成了:第一步為日志消息添加日志時間;第二步具體記錄日志。 那么該怎么實現呢? (1)記錄日志的策略接口沒有變化,為了看起來方便,還是示例一下,示例代碼如下:?? | /** ?* 日志記錄策略的接口 ?*/ public interface LogStrategy { ??? /** ??? ?* 記錄日志 ??? ?* @param msg 需記錄的日志信息 ??? ?*/ ??? public void log(String msg); } |
(2)增加一個實現這個策略接口的抽象類,在里面定義記錄日志的算法骨架,相當于模板方法模式的模板,示例代碼如下:?? | /** ?* 實現日志策略的抽象模板,實現給消息添加時間 ?*/ 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; ?????? //第二步:真正執行日志記錄 ?????? doLog(msg); ??? } ??? /** ??? ?* 真正執行日志記錄,讓子類去具體實現 ??? ?* @param msg 需記錄的日志信息 ??? ?*/????????????????????????????????? ??? protected abstract void doLog(String msg); } |
(3)這個時候那兩個具體的日志算法實現也需要做些改變,不再直接實現策略接口了,而是繼承模板,實現模板方法了。這個時候記錄日志到數據庫的類,示例代碼如下:?? | /** ?* 把日志記錄到數據庫 ?*/ public class DbLog extends LogStrategyTemplate{ ????//除了定義上發生了改變外,具體的實現沒變 ??? 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+"' 記錄到文件中"); ??? } } |
(4)算法實現的改變不影響使用算法的上下文,上下文跟前面一樣,示例代碼如下:?? | /** ?* 日志記錄的上下文 ?*/ public class LogContext { ??? /** ??? ?* 記錄日志的方法,提供給客戶端使用 ??? ?* @param msg 需記錄的日志信息 ??? ?*/ ??? public void log(String msg){ ?????? //在上下文里面,自行實現對具體策略的選擇 ?????? //優先選用策略:記錄到數據庫 ?????? LogStrategy strategy = new DbLog(); ?????? try{ ?????????? strategy.log(msg); ?????? }catch(Exception err){ ?????????? //出錯了,那就記錄到文件中 ?????????? strategy = new FileLog(); ?????????? strategy.log(msg); ?????? } ??? }?? } |
(5)客戶端跟以前也一樣,示例代碼如下:?? | public class Client { ??? public static void main(String[] args) { ?????? LogContext log = new LogContext(); ?????? log.log("記錄日志"); ?????? log.log("再次記錄日志"); ??? } } |
運行一下客戶端再次測試看看,體會一下,看看結果是否帶上了時間。 通過這個示例,好好體會一下策略模式和模板方法模式的組合使用,在實用開發中是很常見的方式。 3.5? 策略模式的優缺點 - 定義一系列算法
策略模式的功能就是定義一系列算法,實現讓這些算法可以相互替換。所以會為這一系列算法定義公共的接口,以約束一系列算法要實現的功能。如果這一系列算法具有公共功能,可以把策略接口實現成為抽象類,把這些公共功能實現到父類里面,對于這個問題,前面講了三種處理方法,這里就不羅嗦了。 - 避免多重條件語句
根據前面的示例會發現,策略模式的一系列策略算法是平等的,可以互換的,寫在一起就是通過if-else結構來組織,如果此時具體的算法實現里面又有條件語句,就構成了多重條件語句,使用策略模式能避免這樣的多重條件語句。 如下示例來演示了不使用策略模式的多重條件語句,示例代碼如下:?
| public class OneClass { ??? /** ??? ?* 示范多重條件語句 ??? ?* @param type 某個用于判斷的類型 ??? ?*/ ??? public void oneMethod(int type){?????? //使用策略模式的時候,這些算法的處理代碼就被拿出去, //放到單獨的算法實現類去了,這里就不再是多重條件了 ? ?????? if(type==1){?????????????????? ?????????? //算法一示范 ?????????? //從某個地方獲取這個s的值 ?????????? String s = ""; ?????????? //然后判斷進行相應處理 ?????????? if(s.indexOf("a") > 0){ ????????????? //處理 ?????????? }else{ ????????????? //處理 ?????????? } ?????? }else if(type==2){ ?????????? //算法二示范 ?????????? //從某個地方獲取這個a的值 ?????????? int a = 3; ?????????? //然后判斷進行相應處理 ?????????? if(a > 10){ ????????????? //處理 ?????????? }else{ ????????????? //處理 ?????????? } ?????? } ??? } } |
- 更好的擴展性
在策略模式中擴展新的策略實現非常容易,只要增加新的策略實現類,然后在選擇使用策略的地方選擇使用這個新的策略實現就好了。 - 客戶必須了解每種策略的不同
策略模式也有缺點,比如讓客戶端來選擇具體使用哪一個策略,這就可能會讓客戶需要了解所有的策略,還要了解各種策略的功能和不同,這樣才能做出正確的選擇,而且這樣也暴露了策略的具體實現。 - 增加了對象數目
由于策略模式把每個具體的策略實現都單獨封裝成為類,如果備選的策略很多的話,那么對象的數目就會很可觀。 - 只適合扁平的算法結構
策略模式的一系列算法地位是平等的,是可以相互替換的,事實上構成了一個扁平的算法結構,也就是在一個策略接口下,有多個平等的策略算法,就相當于兄弟算法。而且在運行時刻只有一個算法被使用,這就限制了算法使用的層級,使用的時候不能嵌套使用。 對于出現需要嵌套使用多個算法的情況,比如折上折、折后返卷等業務的實現,需要組合或者是嵌套使用多個算法的情況,可以考慮使用裝飾模式、或是變形的職責鏈、或是AOP等方式來實現。
3.6? 思考策略模式 1:策略模式的本質 策略模式的本質:分離算法,選擇實現。 仔細思考策略模式的結構和實現的功能,會發現,如果沒有上下文,策略模式就回到了最基本的接口和實現了,只要是面向接口編程的,那么就能夠享受到接口的封裝隔離帶來的好處。也就是通過一個統一的策略接口來封裝和隔離具體的策略算法,面向接口編程的話,自然不需要關心具體的策略實現,也可以通過使用不同的實現類來實例化接口,從而實現切換具體的策略。 看起來好像沒有上下文什么事情,但是如果沒有上下文,那么就需要客戶端來直接與具體的策略交互,尤其是當需要提供一些公共功能,或者是相關狀態存儲的時候,會大大增加客戶端使用的難度。因此,引入上下文還是很必要的,有了上下文,這些工作就由上下文來完成了,客戶端只需要與上下文交互就可以了,這樣會讓整個設計模式更獨立、更有整體性,也讓客戶端更簡單。 但縱觀整個策略模式實現的功能和設計,它的本質還是“分離算法,選擇實現”,因為分離并封裝了算法,才能夠很容易的修改和添加算法;也能很容易的動態切換使用不同的算法,也就是動態選擇一個算法來實現需要的功能了。 2:對設計原則的體現 從設計原則上來看,策略模式很好的體現了開-閉原則。策略模式通過把一系列可變的算法進行封裝,并定義出合理的使用結構,使得在系統出現新算法的時候,能很容易的把新的算法加入到已有的系統中,而已有的實現不需要做任何修改。這在前面的示例中已經體現出來了,好好體會一下。 從設計原則上來看,策略模式還很好的體現了里氏替換原則。策略模式是一個扁平結構,一系列的實現算法其實是兄弟關系,都是實現同一個接口或者繼承的同一個父類。這樣只要使用策略的客戶保持面向抽象類型編程,就能夠使用不同的策略的具體實現對象來配置它,從而實現一系列算法可以相互替換。 3:何時選用策略模式 建議在如下情況中,選用策略模式: - 出現有許多相關的類,僅僅是行為有差別的情況,可以使用策略模式來使用多個行為中的一個來配置一個類的方法,實現算法動態切換
- 出現同一個算法,有很多不同的實現的情況,可以使用策略模式來把這些“不同的實現”實現成為一個算法的類層次
- 需要封裝算法中,與算法相關的數據的情況,可以使用策略模式來避免暴露這些跟算法相關的數據結構
- 出現抽象一個定義了很多行為的類,并且是通過多個if-else語句來選擇這些行為的情況,可以使用策略模式來代替這些條件語句
3.7? 相關模式 - 策略模式和狀態模式
這兩個模式從模式結構上看是一樣的,但是實現的功能是不一樣的。 狀態模式是根據狀態的變化來選擇相應的行為,不同的狀態對應不同的類,每個狀態對應的類實現了該狀態對應的功能,在實現功能的同時,還會維護狀態數據的變化。這些實現狀態對應的功能的類之間是不能相互替換的。 策略模式是根據需要或者是客戶端的要求來選擇相應的實現類,各個實現類是平等的,是可以相互替換的。 另外策略模式可以讓客戶端來選擇需要使用的策略算法,而狀態模式一般是由上下文,或者是在狀態實現類里面來維護具體的狀態數據,通常不由客戶端來指定狀態。 - 策略模式和模板方法模式
這兩個模式可組合使用,如同前面示例的那樣。 模板方法重在封裝算法骨架,而策略模式重在分離并封裝算法實現。 - 策略模式和享元模式
這兩個模式可組合使用。 策略模式分離并封裝出一系列的策略算法對象,這些對象的功能通常都比較單一,很多時候就是為了實現某個算法的功能而存在,因此,針對這一系列的、多個細粒度的對象,可以應用享元模式來節省資源,但前提是這些算法對象要被頻繁的使用,如果偶爾用一次,就沒有必要做成享元了。
|