本号主要用于分享企业中常用的技术,更加侧重于实用,欢迎关注,便于浏览其它更多实用的历史文章。
开发中我们会经常写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 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); }}