本號主要用於分享企業中常用的技術,更加側重於實用,歡迎關注,便於瀏覽其它更多實用的歷史文章。
開發中我們會經常寫if(...) { } else if(...) {} else if (...) {}等多個邏輯判斷,一般else if 會有好幾個,多個else if 代碼塊會使得整個方法看起來比較臃腫,這篇文章的目的就是通過策略設計模式減少甚至消滅else if
1. 最簡單的計算器實現(示例1)
public class Main { public static void main(String[] args) { // 計算類型:1:加法 2:減法 3:乘法 4:除法 // 計算類型和計算的值都是正常客戶端傳過來的 int calcType = 1; int num1 = 1; int num2 = 3; // 計算器真正的實現 int result = 0; if (calcType == 1) { result = num1 + num2; } else if (calcType == 2) { result = num1 - num2; } else if (calcType == 3) { result = num1 * num2; } else if (calcType == 4) { result = num1 / num2; } System.out.println(result); }}上面代碼實現了加減乘除功能,代碼看起來直接、簡潔,但是該示例代碼有幾個問題
計算類型使用了魔法值(就是寫死的值),程序中應該避免直接使用魔法值,可以使用枚舉類來優化;計算器功能的每種操作非常簡單,只需要對兩個數字運算一下就完了,每個操作也就是一行代碼的事,但實際開發過程中每種業務處理可能要幾十行甚至更多行代碼來完成,現在是4種類型,如果每種類型的業務需要100行代碼,這四種就會有400行代碼,這只是if else的代碼量再加上該方法的其它代碼可以想像該方法的代碼量很大,我們需要控制方法的行數,一般通常控制到100行以內,我們可以通過將每個運算的具體實現都提取成一個單獨的方法,這樣一來就縮小每個if else的代碼的行數,只需要一行代碼,只需要調用一個方法就可以了。2. 枚舉類+提取方法實現(示例2)
增加一個枚舉類CalcTypeEnum用於表示所有的運算類型, 使用枚舉類來優化魔法值將每種具體的運算實現提取為獨立的方法public enum CalcTypeEnum { ADD(1, "加法操作"), SUB(2, "減法操作"), MUL(3, "乘法操作"), DIV(4, "除法操作"), ; private Integer code; private String description; CalcTypeEnum(Integer code, String description) { this.code = code; this.description = description; } public Integer getCode() { return code; } public String getDescription() { return description; }}public class Main { public static void main(String[] args) { // 計算類型:1:加法 2:減法 3:乘法 4:除法 int calcType = 1; int num1 = 1; int num2 = 3; int result = 0; // 使用枚舉來代替魔法值 if (CalcTypeEnum.ADD.getCode().equals(calcType)) { // 假如加法運算中還包含其它業務邏輯,那麼這些邏輯也被封裝到方法中了,此處只有一行的代碼量 result = add(num1, num2); } else if (CalcTypeEnum.SUB.getCode().equals(calcType)) { result = sub(num1, num2); } else if (CalcTypeEnum.MUL.getCode().equals(calcType)) { result = mul(num1, num2); } else if (CalcTypeEnum.DIV.getCode().equals(calcType)) { result = div(num1, num2); } System.out.println(result); } /** 將具體的運算實現代碼封裝成獨立的方法 */ public static int add(int num1, int num2) { System.out.println("加法運算其它業務邏輯start"); System.out.println("此處省略幾十行代碼..."); System.out.println("加法運算其它業務邏輯end"); return num1 + num2; } public static int sub(int num1, int num2) { return num1 - num2; } public static int mul(int num1, int num2) { return num1 * num2; } public static int div(int num1, int num2) { return num1 / num2; }}該示例解決了上個示例的缺點,但該示例仍有問題:
main方法屬於客戶端,加減乘除功能是具體的功能實現,用來提供服務,我們不能將服務和客戶端代碼寫到一起,每個類的職責應該更明確,即服務提供方用於專門提供服務,客戶端調用方用於調用服務。
3. 計算器功能單獨封裝成類(示例3)
我們將具體的功能單獨封裝到一個類中用於提供服務
public class Calculator { public int calc(CalcTypeEnum calcType, int num1, int num2) { int result = 0; if (CalcTypeEnum.ADD.equals(calcType)) { result = add(num1, num2); } else if (CalcTypeEnum.SUB.equals(calcType)) { result = sub(num1, num2); } else if (CalcTypeEnum.MUL.equals(calcType)) { result = mul(num1, num2); } else if (CalcTypeEnum.DIV.equals(calcType)) { result = div(num1, num2); } return result; } public int add(int num1, int num2) { System.out.println("加法運算其它業務邏輯start"); System.out.println("此處省略幾十行代碼..."); System.out.println("加法運算其它業務邏輯end"); return num1 + num2; } public int sub(int num1, int num2) { return num1 - num2; } public int mul(int num1, int num2) { return num1 * num2; } public int div(int num1, int num2) { return num1 / num2; }}因為服務方單獨封裝成一個獨立的類,所以服務調用方代碼就減少了很多,顯得很清爽。
public class Main { public static void main(String[] args) { int num1 = 1; int num2 = 3; Calculator calculator = new Calculator(); int result = calculator.calc(CalcTypeEnum.ADD, num1, num2); System.out.println(result); }}這個示例已經比上一個示例優化的好多了,假如我們需要對計算器增強功能,假如我們再為計算器增加一個求餘數的功能,該如何實現呢?
枚舉類CalcTypeEnum增加乘方的枚舉值修改Calculator類,增加一個乘方的方法,然後在calc方法中再增加一個else if 塊該實現的最大的問題是當我們新增一種運算時除了要新增代碼,還需要修改原來的calc代碼,這不太好,我們希望新增運算功能時只需要新增代碼,不要修改原來已有的代碼。
4.將具體運算分別封裝成單獨的類中(示例4)
為了達到新增運算方式不修改代碼只增加代碼的方式,我們需要將每個預算繼續抽象,我們新增一個計算的接口,並將加減乘除求餘數分別封裝到每個具體的實現類裡面。
/** * 計算策略. * */public interface CalcStrategy { int calc(int num1, int num2);}/** * 加法操作 */public class AddStrategy implements CalcStrategy { @Override public int calc(int num1, int num2) { System.out.println("加法運算其它業務邏輯start"); System.out.println("此處省略幾十行代碼..."); System.out.println("加法運算其它業務邏輯end"); return num1 + num2; }}/** * 減法操作. */public class SubStrategy implements CalcStrategy { @Override public int calc(int num1, int num2) { return num1 - num2; }}/** * 乘法操作 */public class MulStrategy implements CalcStrategy { @Override public int calc(int num1, int num2) { return num1 * num2; }}/** * 除法操作 */public class DivStrategy implements CalcStrategy { @Override public int calc(int num1, int num2) { return num1 / num2; }}我們增加一個求餘數的運算只需要增加一個枚舉值並新增一個求餘的實現類, 這樣我們就實現了新增一個功能只需要新增代碼而不用修改之前的代碼的目的。
public enum CalcTypeEnum { ADD(1, "加法操作"), SUB(2, "減法操作"), MUL(3, "乘法操作"), DIV(4, "除法操作"), // 求餘數運算 REM(5, "求餘操作"), ;}/** * 求餘操作 */public class RemStrategy implements CalcStrategy { @Override public int calc(int num1, int num2) { return num1 % num2; }}public class Main { // 用戶要計算的類型 private static final int CALC_TYPE = 5; public static void main(String[] args) { // 根據用戶要運算的類型調用相應實現類的方法 CalcStrategy calcStrategy = null; if (CalcTypeEnum.ADD.getCode().equals(CALC_TYPE)) { calcStrategy = new AddStrategy(); } else if (CalcTypeEnum.SUB.getCode().equals(CALC_TYPE)) { calcStrategy = new SubStrategy(); } else if (CalcTypeEnum.MUL.getCode().equals(CALC_TYPE)) { calcStrategy = new MulStrategy(); } else if (CalcTypeEnum.DIV.getCode().equals(CALC_TYPE)) { calcStrategy = new DivStrategy(); } else if (CalcTypeEnum.REM.getCode().equals(CALC_TYPE)) { calcStrategy = new RemStrategy(); } int result = calcStrategy.calc(10, 20); System.out.println(result); }}5. 為示例4引入上下文(示例5)
上下文只持有一個運算接口的引用並提供一個執行策略的方法,這裡的方法實現也很簡單,就是簡單調用具體運算實現類的方法
public class CalcStrategyContext { // 運算接口 private CalcStrategy strategy; // 通過構造函數或者set方法賦值 public CalcStrategyContext(CalcStrategy strategy) { this.strategy = strategy; } public int executeStrategy(int a, int b) { // 簡單調用具體實現對應的方法 return strategy.calc(a, b); } public CalcStrategy getStrategy() { return strategy; } public void setStrategy(CalcStrategy strategy) { this.strategy = strategy; }}public class Main { private static final Integer CALC_TYPE = 1; public static void main(String[] args) { CalcStrategy calcStrategy = null; if (CalcTypeEnum.ADD.getCode().equals(CALC_TYPE)) { calcStrategy = new AddStrategy(); } else if (CalcTypeEnum.SUB.getCode().equals(CALC_TYPE)) { calcStrategy = new SubStrategy(); } else if (CalcTypeEnum.MUL.getCode().equals(CALC_TYPE)) { calcStrategy = new MulStrategy(); } else if (CalcTypeEnum.DIV.getCode().equals(CALC_TYPE)) { calcStrategy = new DivStrategy(); } // 這裡不再直接調用接口方法了,而是調用上下文類中的方法 // 上下文就是對接口的一種簡單的裝飾和封裝 CalcStrategyContext context = new CalcStrategyContext(calcStrategy); int result = context.executeStrategy(20, 30); System.out.println(result); }}此示例和上個示例不同的就是調用方法的對象不同,一個是直接調用CalcStrategy#calc接口的方法,一個是調用CalcStrategyContext#executeStrategy上下文中的方法,上下文中的方法也是簡單的調用具體的實現類,在這裡感覺上下文沒太大的意義。
上下文存在的意義?
現在加入我們要實現一個促銷的功能,假如目前支持 滿x元減y元、第x件y折、滿x件y折等多種促銷方式,假如業務規定當滿足多種促銷時取所有促銷的最低價。要想實現這種功能,我們首先找出當前訂單能夠享受的促銷類型,然後分別計算每種促銷類型促銷後的促銷價,然後比較所有促銷類型對應的促銷價,取最低的促銷價格。
上下文中大部分情況下是直接調用調用接口的方法,但是也有一些情況是需要在上下文中處理一些邏輯,處理不同實現的依賴關係。
/** * 促銷上下文 * @author Mengday Zhang * @version 1.0 * @since 2019-08-16 */public class PromotionContext { public BigDecimal executeStrategy() { BigDecimal promotionPrice = null; List<PromotionStrategy> promotionList = ...; for (PromotionStrategy strategy : promotionList) { // 執行下次運算需要將上次運算的結果作為下次運算的參數, // 每次計算的結果需要和傳進來的上次運算結果做比較,取最小值作為新的結果返回 promotionPrice = strategy.executeStrategy(promotionPrice); } return promotionPrice; }}到此為止,策略模式的所有元素都體現出來了,沒錯示例5就是策略模式的具體實現。
策略設計模式的特點
提供一個策略接口提供多個策略接口的實現類提供一個策略上下文
策略設計模式優點
可以自由切換算法(具體實現)避免了多條件的判斷(幹掉了if else)擴展性好可以定義新的算法提供給使用者(增加新功能時只需要增加代碼而不需要修改代碼)策略設計模式缺點
算法類數量增多,每個算法都是一個類,這對於初級程式設計師比較難以接受6. 通過枚舉類撤掉幹掉if else(示例6)
上個示例我們看到仍然有很多if else代碼,我們需要減少甚至消滅這種代碼,消滅if else這裡列舉兩種方式
, 一種是通過配置枚舉類。在枚舉中增加對應的運算實現類,並提供一個根據code來獲取對應的枚舉類的方法, 獲取到枚舉類了就獲取到對應的實現類了。
public enum CalcTypeEnum { // code 一般設置為具體實現類的前綴 ADD("Add", "加法操作", new AddStrategy()), SUB("Sub", "減法操作", new SubStrategy()), MUL("Mul", "乘法操作", new MulStrategy()), DIV("Div", "除法操作", new DivStrategy()), ; private String code; private String description; private CalcStrategy calcStrategy; CalcTypeEnum(String code, String description, CalcStrategy calcStrategy) { this.code = code; this.description = description; this.calcStrategy = calcStrategy; } // 根據code獲取對應的枚舉類型 public static CalcTypeEnum getCalcTypeEnum(String code) { for (CalcTypeEnum calcTypeEnum : CalcTypeEnum.values()) { if (calcTypeEnum.getCode().equals(code)) { return calcTypeEnum; } } return null; } public String getCode() { return code; } public String getDescription() { return description; } public CalcStrategy getCalcStrategy() { return calcStrategy; }}public class Main { private static final String CALC_TYPE = "Sub"; public static void main(String[] args) { // 消除if else,根據code獲取到對應的枚舉類,進而獲取到對應的計算實現類 CalcStrategy addStrategy = CalcTypeEnum.getCalcTypeEnum(CALC_TYPE).getCalcStrategy(); CalcStrategyContext context = new CalcStrategyContext(addStrategy); int result = context.executeStrategy(20, 30); System.out.println(result); }}7. 通過反射徹底幹掉if else(方式二)
我們將每個具體的實現類變成單例模式,這裡通過懶漢模式來實現單例類。
/** * 加法操作 */public class AddStrategy implements CalcStrategy { private static AddStrategy addStrategy; private AddStrategy() { } // 懶漢模式實現單例類 public static AddStrategy getInstance() { if (addStrategy == null) { addStrategy = new AddStrategy(); } return addStrategy; } @Override public int calc(int num1, int num2) { System.out.println("加法運算其它業務邏輯start"); System.out.println("此處省略幾十行代碼..."); System.out.println("加法運算其它業務邏輯end"); return num1 + num2; }}通過反射獲取某個具體的類,然後調用具體類的getInstance方法,從而獲取對應的運算實現類。
public class CalcStrategyUtils { public static CalcStrategy getCalcStrategy(String calcType) { try { // 這裡包名是寫死的,開發時需要將實現類統一放到該包下面,類的命名也是有規則的,以Strategy作為後綴 String path = "com.example.design.strategy.demo7." + calcType.concat("Strategy"); Class<?> clazz = Class.forName(path); CalcStrategy instance = (CalcStrategy) clazz.getDeclaredMethod("getInstance").invoke(null, null); return instance; } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("Load [" + calcType.concat("Strategy") + "] Error :", e); } }}public class Main { private static final String CALC_TYPE = "Add"; public static void main(String[] args) { // 通過反射獲取到具體實現類型,從而消除if else CalcStrategy addStrategy = CalcStrategyUtils.getCalcStrategy(CALC_TYPE); CalcStrategyContext context = new CalcStrategyContext(addStrategy); int result = context.executeStrategy(20, 30); System.out.println(result); }}