策略模式——(+簡單工廠模式+反射)


策略模式,需要我們結合簡單工廠模式,更高級地用法可能需要我們掌握Java反射機制。簡單工廠模式我們在最早的時候介紹,我們也談到了一點Java的反射機制。借着學習策略模式的機會,我們順便復習一下簡單工廠模式和反射。

先說說何為策略模式。“策略”我的理解是,對一件事,有不同的方法去做,至於用何種方法取決於我們的選擇。我們同樣借助《大話設計模式》中實現策略模式的例子來做講解。

超市進場做活動,我們現在假設有正常不減價、打折、滿減這三種活動,這正是對“買東西收費”這件事,有三種不同的“方法”,這三種方法其實就是三種不同的算法。我們定義出策略模式:它定義了算法家族,分別封裝起來,讓它們之間可以互相替換,此模式讓算法的變化不會影響到使用算法的客戶。看到這個可能還是一臉茫然,不着急我們一步一步來這句話到底想表達什么意思。

首先,對於正常不減價,我們可以直接計算返回該收取的金額為多少。對於打折的這種情況,我們可能會想到傳遞一個“打多少折”的參數進去,計算返回該收取的金額為多少。對於滿減的這種情況,我們傳遞兩個參數,“返利條件”及“返多少利”,計算返回該收取的金額為多少。那么它們似乎都有一個公共方法,對於應收金額,返回實收金額。我們可以將三種情況抽取為一個接口或抽象類。來試着畫出UML類圖結構。

看到UML的類結構圖,我們其實可以聯想到簡單工廠模式,如果我們就這樣來寫,在客戶端就需要來具體實例化哪一個類。我們不想在客戶端來做出判斷決定來實例化哪一個類,這個時候怎么辦呢——簡單工廠模式可以幫我們實現。客戶端不決定具體實例化哪一個類,而是交由“工廠”來幫我們實例化。所以其實我們首先是實現的一個“簡單工廠模式”。

所以我們上面的UML類結構圖就可以做下修改。

接下來寫出我們的代碼。

 1 package day_20_cash;
 2 
 3 /**
 4  * 收費接口
 5  * @author turbo
 6  *
 7  * 2016年9月20日
 8  */
 9 public interface CashSuper {
10     /**
11      * 計算實收的費用
12      * @param money 應收金額
13      * @return 實收金額
14      */
15     double acceptCash(double money);
16 }
 1 package day_20_cash;
 2 
 3 /**
 4  * 正常收費
 5  * @author turbo
 6  *
 7  * 2016年9月20日
 8  */
 9 public class CashNormal implements CashSuper {
10 
11     /* (non-Javadoc)
12      * @see day_20_cash.CashSuper#acceptCash(double)
13      */
14     @Override
15     public double acceptCash(double money) {
16 
17         return money;
18     }
19 
20 }
 1 package day_20_cash;
 2 
 3 /**
 4  * 打折
 5  * @author turbo
 6  *
 7  * 2016年9月20日
 8  */
 9 public class CashRebate implements CashSuper {
10     private double moneyRebate;
11     
12 
13     /**
14      * @param moneyRebate 折扣率
15      */
16     public CashRebate(double moneyRebate) {
17         this.moneyRebate = moneyRebate;
18     }
19 
20 
21     /* (non-Javadoc)
22      * @see day_20_cash.CashSuper#acceptCash(double)
23      */
24     @Override
25     public double acceptCash(double money) {
26         
27         return money * (moneyRebate / 10);
28     }
29 
30 }
 1 package day_20_cash;
 2 
 3 /**
 4  * 滿減
 5  * @author turbo
 6  *
 7  * 2016年9月20日
 8  */
 9 public class CashReturn implements CashSuper {
10     private double moneyCondition;    //應收金額
11     private double moneyReturn;    //返利金額
12     
13     public CashReturn(double moneyCondition, double moneyReturn){
14         this.moneyCondition = moneyCondition;
15         this.moneyReturn = moneyReturn;
16     }
17     /* (non-Javadoc)
18      * @see day_20_cash.CashSuper#acceptCash(double)
19      */
20     @Override
21     public double acceptCash(double money) {
22         if (money >= moneyCondition){
23             money = money - moneyReturn;
24         }
25         return money;
26     }
27 
28 }
 1 package day_20_cash;
 2 
 3 /**
 4  * 收費對象生成工廠
 5  * @author turbo
 6  *
 7  * 2016年9月20日
 8  */
 9 public class CashFactory {
10     public static CashSuper createCashAccept(String cashType){
11         CashSuper cs = null;
12         switch (cashType) {
13             case "正常收費" :
14                 cs = new CashNormal();
15                 break;
16             case "打8折" :
17                 cs = new CashRebate(8);
18                 break;
19             case "滿300減100" :
20                 cs = new CashReturn(300, 100);
21                 break;
22             default :
23                 break;
24         }
25         
26         return cs; 
27     }
28 }
 1 package day_20_cash;
 2 
 3 /**
 4  * 客戶端抽象代碼
 5  * @author turbo
 6  *
 7  * 2016年9月20日
 8  */
 9 public class Main {
10 
11     /**
12      * @param args
13      */
14     public static void main(String[] args) {
15         CashSuper cs = CashFactory.createCashAccept("打8折");
16         double result = cs.acceptCash(300);
17         System.out.println(result);
18     }
19 
20 }

這樣雖然在客戶端中,我們不用關系具體實體化哪一個類,但這同樣也帶來一定的問題,如果我們要打7折呢?我們是否要在工廠類中新增一個case?那滿500減100呢?商場的活動經常在改變,如果真向我們現在所寫的這樣未免有些牽強,我們要不斷地去修改工廠類,不斷地重新編譯重新部署。面對算法的時常變動,我們可以選擇策略模式。

對於策略模式,我們需要引入一個CashContext類,這個類用於維護對Strategy對象的引用。還是太抽象,我們從代碼的角度來看,CashContext是一個什么類。(上面的CashSuper及其實現類不用修改)

 1 package day_20_cash;
 2 
 3 /**
 4  * Context上下文,維護對strategy對象的引用
 5  * @author turbo
 6  *
 7  * 2016年9月21日
 8  */
 9 public class CashContext {
10     CashSuper cs = null;
11     public CashContext(CashSuper csuper){
12         this.cs = csuper;
13     }
14     
15     public double getResult(double money){
16         
17         return cs.acceptCash(money);
18     }
19 }

再來看客戶端代碼怎么寫。

 1 package day_20_cash;
 2 
 3 /**
 4  * 客戶端抽象代碼
 5  * @author turbo
 6  *
 7  * 2016年9月20日
 8  */
 9 public class Main {
10 
11     /**
12      * @param args
13      */
14     public static void main(String[] args) {
15         CashContext context = null;
16         double money = 0.0;
17         String strategy = "打8折";
18         switch (strategy) {
19             case "正常收費" :
20                 context = new CashContext(new CashNormal());
21                 break;
22             case "打8折" :
23                 context = new CashContext(new CashRebate(8));
24                 break;
25             case "滿300減100" :
26                 context = new CashContext(new CashReturn(300, 100));
27                 break;
28 
29             default :
30                 break;
31         }
32         
33         money = context.getResult(300);
34         System.out.println(money);
35     }
36 
37 }

這樣我們就實現了策略模式。

但是,我們又再一次客戶端做了判斷,實際上我們似乎是將switch語句從工廠移到了客戶端,這不又違背我們的初衷回到原點了嗎?那我們是否能將switch“又移到”工廠中去呢?換句話說,策略模式和工廠模式相結合。

我們改進CashContext在其中實現簡單工廠。

 1 package day_20_cash;
 2 
 3 /**
 4  * Context上下文,維護對strategy對象的引用
 5  * @author turbo
 6  *
 7  * 2016年9月21日
 8  */
 9 public class CashContext {
10     CashSuper cs = null;
11     public CashContext(String type){
12         switch (type) {
13             case "正常收費" :
14                 CashNormal normal = new CashNormal();
15                 cs = normal;
16                 break;
17             case "滿300減100" :
18                 CashReturn returnx = new CashReturn(300, 100);
19                 cs = returnx;
20             case "打8折" :
21                 CashRebate rebate = new CashRebate(8);
22                 cs = rebate;
23             default :
24                 break;
25         }
26     }
27     
28     public double getResult(double money){
29         
30         return cs.acceptCash(money);
31     }
32 }

客戶端測試代碼:

 1 package day_20_cash;
 2 
 3 /**
 4  * 客戶端抽象代碼
 5  * @author turbo
 6  *
 7  * 2016年9月21日
 8  */
 9 public class Main {
10 
11     /**
12      * @param args
13      */
14     public static void main(String[] args) {
15         CashContext context = null;
16         double money = 0.0;
17         String strategy = "打8折";
18         context = new CashContext(strategy);
19         money = context.getResult(300);
20         System.out.println(money);
21     }
22 
23 }

從代碼角度來看,不就是把switch從Main客戶端類移到了CashContext類嘛,好像根本沒什么用啊。我們用書里的解釋吧,“簡單工廠模式需要讓客戶端認識兩個類,CashSuper和CashFactory,而策略模式與簡單工廠結合的用法,客戶端就只需要認識一個類CashContext就可以了。耦合更加降低。”“我們在客戶端實例化的是CashContext的對象,調用的是CashContext的方法getResult,這使得具體的收費算法徹底地與客戶端分離。連算法的父類CashSuper都不讓客戶端認識了。

在這里我們要領會“客戶端”帶來的含義是什么,在這里我們就是寫的一個main函數,“客戶端”在編碼過程中,我們可以把它想象理解為調用方。調用方如果引用多個類是不是帶來很大的耦合性?但如果只引用一個類,那是不是只需要維護這個類的引用即可?這也就是我們常說的解耦。

下面我們來實現在最開始提到的使用“反射”來去掉switch判斷語句,可以先自己思考一下試着自己寫出來。這里可以參考一下之前涉及到一點反射的博文,《初識Java反射》《工廠模式——抽象工廠模式(+反射)》

修改CashContext類,利用反射消除Switch判斷語句:

 1 package day_20_cash;
 2 
 3 import java.lang.reflect.Constructor;
 4 import java.lang.reflect.InvocationTargetException;
 5 
 6 /**
 7  * Context上下文,維護對strategy對象的引用
 8  * @author turbo
 9  *
10  * 2016年9月22日
11  */
12 public class CashContext {
13     Class<?> clazz = null;
14     Object obj = null;
15     public CashContext(String className, Class[] paramsType, Object[] parmas){
16         try {
17             clazz = Class.forName(className);
18             Constructor con = clazz.getConstructor(paramsType);
19             obj = con.newInstance(parmas);
20         } catch (InstantiationException | IllegalAccessException e) {
21             e.printStackTrace();
22         } catch (ClassNotFoundException e) {
23             e.printStackTrace();
24         } catch (IllegalArgumentException e) {
25             e.printStackTrace();
26         } catch (InvocationTargetException e) {
27             e.printStackTrace();
28         } catch (NoSuchMethodException e) {
29             e.printStackTrace();
30         } catch (SecurityException e) {
31             e.printStackTrace();
32         }
33         
34     }
35     
36     public double getResult(double money){
37         
38         return ((CashSuper)obj).acceptCash(money);
39     }
40 }

修改客戶端測試代碼:

 1 package day_20_cash;
 2 
 3 /**
 4  * 客戶端測試代碼
 5  * @author turbo
 6  *
 7  * 2016年9月22日
 8  */
 9 public class Main {
10 
11     /**
12      * @param args
13      */
14     public static void main(String[] args) {
15         CashContext context = null;
16         double money = 0.0;
17         String type = "day_20_cash.CashRebate";
18         Class[] paramTypes = {double.class};    //注意在這里不能使用double的引用類型Double,我猜測是這樣涉及一點自動裝箱和拆箱
19         Object[] params = {8.0}; 
20         context = new CashContext(type, paramTypes, params);
21         money = context.getResult(300);
22         System.out.println(money);
23     }
24 
25 }

至於為什么要用到反射來消除switch,在上面兩篇博文中已經有提到過,這里不再敘述。其實在客戶端測試代碼中,我們還可以進一步把代碼寫得再優美一點。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM