策略模式,需要我們結合簡單工廠模式,更高級地用法可能需要我們掌握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,在上面兩篇博文中已經有提到過,這里不再敘述。其實在客戶端測試代碼中,我們還可以進一步把代碼寫得再優美一點。