基于策略模式企业实战中策略命中设计
背景
在公司实际项目项目开发中,有一个策略命中的开发需求。根据用户请求参数的不同来动态返回不同的业务数据。比如说有城市、客户年龄、请求时间3个策略维度,不同的城市返回不同的地区的地标,根据时间地标的背景色要发生变化等等的需求。当然,如果你直接使用一堆的嵌套if/else来硬代码编写这个业务的话,确实也行。那难度再升级一下,有20个策略维度的话,这代码谁维护谁跑路。
所以我们要用合理规范的设计模式来实现该功能。
方案拟定
先来了解一下策略模式的概念:
策略模式是一种设计模式,它定义了一系列算法,并将每个算法封装起来,使他们可以相互替换。策略模式让算法独立于使用它的客户而独立变化。
在Java中,可以使用如下步骤实现策略模式:
- 定义策略接口,该接口定义了所有算法的公共接口。
- 实现策略接口,为每种算法实现一个具体策略类。
- 创建一个上下文类,该类持有一个策略类的引用,并且实现策略接口的方法。
- 客户端代码中,创建一个上下文对象,并设置一个具体策略对象。
策略模式的意义在于:
- 可以让算法和业务代码分离,使得算法可以独立演化,不会影响业务代码。
- 可以在不修改业务代码的情况下更换算法,提高了系统的灵活性。
- 可以很容易地扩展新的算法,而不需要修改原有的代码。
- 可以减少代码的冗长,使代码更加清晰易读。
简单来说的话,就是创建一个接口,该接口定义一个公共的方法,实现类继承这个接口,实现具体的功能。再创建一个service类,对外提供服务时是通过这个类去动态传入实际处理逻辑的实现类去完成任务。
需求分析
结合实际业务场景来设计代码演练一下,拿上面的需求来开发。案例中我们来实现城市、客户年龄、请求时间3个策略维度的命中功能判断服务。
功能刨析:
- 需要一个定义了策略接口
IStrategy
,接口中有执行策略判断方法executeStrategy()
; - 城市策略
CityStrategy
、客户年龄AgeStrategy
、请求时间策略RquestTimeStrategy
都实现IStrategy
接口 - 策略实现类
StrategyService
是对外实现策略命中的类
大局来看主要是这3部分,细节我们在下面的章节来补充。
策略模式架构代码实现
准备工作
- 封装请求报文中的策略类
@Data
public class RequestStrategyInfo {private String cityInfo;private String ageInfo;private String requestTime;....
}
1. 定义策略统一接口
public interface IStrategy {boolean executeStrategy();
}
2. 各个策略实现IStrategy
接口
城市策略
public class CityStrategy implements IStrategy {@Overridepublic boolean executeStrategy() {}
}
年龄策略
public class AgeStrategy implements IStrategy {@Overridepublic boolean executeStrategy() {}
}
请求时间策略
public class RquestTimeStrategy implements IStrategy {@Overridepublic boolean executeStrategy() {}
}
3. StrategyService
对外提供策略匹配服务
StrategyService是最重要的,在这个类里,它要识别出当前判断的策略需要调用哪个具体的策略实例来执行,这有好几种方法:
- 策略工厂
import java.lang.reflect.InvocationTargetException;public class StrategyFactory {@Overridepublic IStrategy createStrategy(String strategyType) {try {Class<?> strategyClass = Class.forName(strategyType);return (IStrategy) strategyClass.getDeclaredConstructor().newInstance();} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {e.printStackTrace();}return null;}
}
public class StrategyService {private IStrategy strategy;private StrategyFactory strategyFactory;public boolean executeStrategy() {// 是否支持任意策略if (strategyInfo.getIsAll()) {return true;}// 获取从数据库详细的策略信息StrategyItem si = strategyItemService.qryStrategyItem(itemId);String strategyType = si.getStrategyType();// 根据strategyType获得对应的策略实例strategy = strategyFactory.createStrategy(strategyType);// 执行策略匹配规则return strategy.executeStrategy();}
}
这种方法可以完成动态找到需要的实现类,不过要求strategyType与类名有强关联关系,必须要通过strategyType来创建实例,也可通过定义枚举类来进一步解耦。
- 在上面的基础上定义枚举类
StrategyEnum
进行解耦
public enum StrategyType {CityStrategy("city", CityStrategy.class),AgeStrategy("age", AgeStrategy.class),RequestTimeStrategy("requestTime", RequestTimeStrategy.class);private String strategyType;private Class<? extends IStrategy> strategyClass;SortType(String sortType, Class<? extends IStrategy> strategyClass) {this.strategyType = strategyType;this.strategyClass = strategyClass;}public Class<? extends IStrategy> getStrategyClassBySortType(String strategyType) {StrategyType[] values = StrategyType.values();return Arrays.stream(values).filter(it -> it.strategyType.equals(strategyType)).findFirst().get().strategyClass;}
}
StrategyService需要稍微改造
public class StrategyService {private IStrategy strategy;private StrategyFactory strategyFactory;public boolean executeStrategy() {// 是否支持任意策略if (strategyInfo.getIsAll()) {return true;}// 获取从数据库详细的策略信息StrategyItem si = strategyItemService.qryStrategyItem(itemId);String strategyType = si.getStrategyType();// 根据strategyType获得对应的策略实例// strategy = strategyFactory.createStrategy(strategyType);try {strategy = StrategyType.getStrategyClassBySortType(strategyType).newInstance();} catch (InstantiationException | IllegalAccessException e) {e.printStackTrace();}// 执行策略匹配规则return strategy.executeStrategy();}
}
使用枚举可以解耦strategyType和策略类名的关系,在枚举里面维护对应关系,以后如果有新的策略,直接添加枚举即可。
- 如果第二种方法不喜欢的话,可以通过在StrategyService中定义一个Map集合维护strategyType与类名的关系
public class StrategyService {private Map<String, Class<? extends IStrategy>> strategyMap;public StrategyService() {strategyMap = new HashMap<>();strategyMap.put("city", CityStrategy.class);strategyMap.put("age", AgeStrategy.class);strategyMap.put("requestTime", RequestTimeStrategy.class);}private IStrategy strategy;private StrategyFactory strategyFactory;public boolean executeStrategy() {// 是否支持任意策略if (strategyInfo.getIsAll()) {return true;}// 获取从数据库详细的策略信息StrategyItem si = strategyItemService.qryStrategyItem(itemId);String strategyType = si.getStrategyType();// 根据strategyType获得对应的策略实例// strategy = strategyFactory.createStrategy(strategyType);/*try {strategy = StrategyType.getStrategyClassBySortType(strategyType).newInstance();} catch (InstantiationException | IllegalAccessException e) {e.printStackTrace();}*/Class<? extends IStrategy> strategyClass = strategyMap.get(strategyType);if (Class<? extends IStrategy> strategyClass = strategyMap.get(strategyType);== null) {throw new IllegalArgumentException("Unsupported strategy type: " + strategyType);}try {strategy = strategyClass.getDeclaredConstructor().newInstance();} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {e.printStackTrace();}// 执行策略匹配规则return strategy.executeStrategy();}
}
用Map维护映射的话,需要考虑策略的数量,如果数量巨大的话,对性能开销可不容小看的;而且这种映射关系手动维护在类里面的做法,可能不太优雅。
- 如果你觉得上述3种方法的硬代码、耦合度都达不到你公司的标准,你可以选择程序在初始化时动态加载全部 IStrategy 接口下的实现类实例到ArrayList中的方式
在自动选择策略时,会遍历一遍所有的策略是否支持当前操作策略类型。在选择使用这种方式前,需要对IStrategy接口进行改造:
public interface IStrategy {boolean executeStrategy();// 当前实现类是否支持boolean support(String strategyType);
}
实现类中对support()方法进行完善,举个例子,比如城市策略:
public class CityStrategy implements IStrategy {@Overridepublic boolean executeStrategy() {}@Overridepublic boolean support(String strategyType) {return "CityStrategy".equals(strategyType);}
}
其余策略方式一致,可考虑将字符串定义成常量在IStrategy接口中,这样会更规范一点
在StrategyService中使用时,需要在初始化时先将IStrategy接口下的所有实现类放到ArrayList中,提供给executeStrategy()方法使用
public class StrategyService implements InitializingBean {@Autowiredprivate ApplicationContext appContext;private Collection<IStrategy> strategys;private IStrategy strategy;private StrategyFactory strategyFactory;public boolean executeStrategy() {// 是否支持任意策略if (strategyInfo.getIsAll()) {return true;}// 获取从数据库详细的策略信息StrategyItem si = strategyItemService.qryStrategyItem(itemId);String strategyType = si.getStrategyType();// 根据strategyType获得对应的策略实例IStrategy strategy = strategys.parallelStream().filter(it -> it.support(strategyType)).findFirst().get();// 执行策略匹配规则return strategy.executeStrategy();}@Overridepublic void afterPropertiesSet() throws Exception() {Map<String, IStrategy> strategyMap = appContext.getBeansOfType(IStrategy.class);strategys = strategyMap.values();}
}
这种方法不需要保证strategyType和策略类名的一一对应关系,是解耦最为激进的一种方式。但是如果策略数量非常多,遍历整个列表可能带来性能问题。
没有最好的方法,只有最适合的方法。如果策略数量不多,那么使用Map来存储映射关系是一个好的选择。如果策略数量非常多,而且不需要频繁地添加和移除策略,那么使用抽象的策略工厂是一个不错的选择。如果策略数量非常多,进一步考虑一定的解耦性,可添加枚举类解耦。如果对解耦的需求更重要于性能考虑,可考虑使用ArrayList遍历的方法。
业务绑定策略设计与代码实现
上一章节只讲述了策略模式架构的设计与不同场景选择策略实例的方式,这一章相当于上一章节的前传。要先有业务绑定了策略之后,请求业务才有策略匹配。
【根据用户请求参数的不同来动态返回不同的业务数据。】这个业务的场景是有一个管理后台,去设定某个业务绑定某些策略。当客户端带着请求参数来访问该业务时,我们要对请求参数的某些字段的值与该业务选择的策略值进行一一比较,若全都符合时,方可返回该业务数据。
实体类设计
城市、年龄、时间等这些定义为策略类型,城市中包含广州、上海、北京、深圳,年龄有18、19、20这些定义为策略值。
策略类型实体类为StrategyType
@Data
public class StrategyType {private Long id; // 主键private String type; // 类型private String name; // 类型名称private String oper; // 支持的操作
}
策略值实体类为StrategyValue
public class StrategyValue {private Long id; // 主键private Long typeId; // 策略类型idprivate String value; // 策略值
}
单单有策略类型与其具体的策略值是不够的,业务如何绑定策略?我们假设操作上是这样的一个流程:在创建好一个业务之后,选择关联策略,城市选择广州,年龄选择等于(大于、小于)18,点击保存。捋一下这两者的关系不难发现,业务与策略值之间是多对多的关系。数据库层面还需要要建一个关联表。
关键字段有:业务id、策略值id、操作类型
操作类型有:等于、大于、小于、不等于;
这个思路去做可以实现灵活度非常高的策略配置,但缺点就是如果每个业务都需要配置多个策略,尤其是一些策略几乎每个都要配置的,那每次都要进行相同的多次操作确实很烦人。
在这里我给出的优化建议有两个,第一个是保存历史操作记录,业务添加策略时读取操作记录表的数据,操作记录保存多少条这个看具体情况而定;第二个是新增一个复合策略表,可以组合一些常用的复合策略,比如广州市18岁策略,在选择策略时可以选择复合策略。
业务绑定策略并没有什么复杂操作,代码主要与业务代码镶嵌,不方便举例。查找出业务绑定的策略时,封装成一个对象。
@Data
public class StrategyDetail {private Long id;private String type;private String oper;private String value;
}
策略匹配设计
这一章节讲述客户端传入策略与数据库中的策略匹配的设计。你可以理解成,这一章节将重点讲述IStrategy接口中的executeStrategy()的实现。
有些同学可能就认为,就这有什么难的,我在每个策略实现类中都用业务策略.equals(请求参数)
不就无敌了?如果说只有【等于】这么一种操作类型确实可以这么做,但我们有多种操作类型,不能在每个策略实现类都写一遍重复性的代码。
封装一个策略匹配工具
public class StrategyUtil {// 匹配public static boolean matchStrategy(RequestStrategyInfo requestInfo, StrategyDetail detail) {Object reqVal = null;try {reqVal = getReqVal(requestInfo, detail.getType());}return computeOper(detail.getOper(), detail.getValue(), reqVal);}// 获取请求对象中的值private Object getReqVal(RequestStrategyInfo requestInfo, String type) {// 利用反射来获取请求对象中的值,但要求策略类型的值要与RequestStrategyInfo类的属性同名Field f = RequestStrategyInfo.class.getDeclareField(type);f.setAccessible(true);return f.get(requestInfo);}// 对比值private boolean computeOper(String oper, String value, Object val) {boolean result = false;switch (oper) {case "0":result = value.equals(val);break;case "1": // 小于result = Double.parseDouble(val) < Double.parseDouble(value);break;case "2": // 大于result = Double.parseDouble(val) > Double.parseDouble(value);break;case "3":result = !value.equals(val);break;}return result;}
}
我们来举个城市策略的调用例子
public class CityStrategy implements IStrategy {@Overridepublic boolean executeStrategy(RequestStrategyInfo requestInfo, StrategyDetail detail) {boolean result = StrategyUtil.matchStrategy(requestInfo, detail);// 若有特殊处理操作可在此加上return result;}
}
几乎所有的策略实现类都是这样的一行代码,如果有特殊操作可以补充在后面。
与前面的选择策略实现类相结合
public class StrategyService implements InitializingBean {@Autowiredprivate ApplicationContext appContext;private Collection<IStrategy> strategys;private IStrategy strategy;private StrategyFactory strategyFactory;// 由业务类自己提供所有的策略详情public boolean executeStrategy(List<strategyDetails> strategyDetails) {// 是否支持任意策略,没有策略则说明支持所有字段if (strategyDetails.size() == 0 || strategyDetails == null) {return true;}List<Boolean> matchList = new ArrayList<>();for (StrategyDetail sd : strategyDetails) {boolean b = strategys.parallelStream().filter(it -> it.support(sd.type)).findFirst().get().executeStrategy(requestInfo, sd);matchList.add(b);}boolean b = matchList.stream().filter(it -> it == false).findFirst().orElse(null);return b == null ? true : false;}@Overridepublic void afterPropertiesSet() throws Exception() {Map<String, IStrategy> strategyMap = appContext.getBeansOfType(IStrategy.class);strategys = strategyMap.values();}
}
在业务实现类的实现思路
public class XxxServiceImpl {@Autowiredprivate StrategyService strategyServicepublic void xxx() {// 1. 处理业务......// 2. 根据业务id找出全部的关联策略详情List<StrategyDetail> strategyDetails = xxxx.getByServiceId(xx);boolean support = strategyService.executeStrategy(strategyDetails);if (support) {// 支持则进行的操作} else {// 不支持的操作}......}
}
总结
基于策略模式设计的策略命中设计,主要难点是策略模式的框架设计理念与策略值对比。