策略模式Spring框架下开发实例
策略类Spring框架下开发实例
先列出策略模式下需要那些类:
策略接口 (
Strategy
),定义所有策略类必须遵循的行为。具体策略类(如
ConcreteStrategyA
、ConcreteStrategyB
),实现不同的算法或行为。上下文类 (
Context
),持有策略实例并根据条件动态选择和创建策略。辅助类和注解(作为工具辅助构建类工厂,可忽略),为策略模式的实现提供额外的功能或配置。
具体实现
定义策略接口
采用策略模式的情况一般是封装多重if-else/switch下的不同处理逻辑,这些逻辑可以抽象为一个行为(比如封装参数,做前置校验等),将这个行为定义为接口方法
/**
* 策略接口
*/
public interface ProductTypeHandler {
//本例中根据不同产品类型执行不同的返回参数封装逻辑
void assemble(ResponseBO resBO);
}
定义策略类及其处理逻辑
将上文提到的多重if-else/switch中不同情况及其处理逻辑抽象成具体策略类和其内部方法执行逻辑
/**
* 记录器参数处理类
*/
@Component
//注解使用见3
@ProductTypeAssembleTag(TAG = EnumProductType.PROCESS_RECORDER)
public class RecorderHandler implements ProductTypeHandler {
@Autowired
private RecorderService service;
@Override
public void assemble(ResponseBO resBO) {
//记录器的处理逻辑
int bindDeviceNums = service.getNumByUserId(ContextUtil.getUserId());
resBO.setValue(bindDeviceNums);
}
}
@Component
@ProductTypeAssembleTag(TAG = EnumProductType.COMMANDER)
//继承见4
public class CommanderHandler extends CommonHandler implements ProductTypeHandler {
@Override
public void assemble(ResponseBO resBO) {
//控制器的处理逻辑
...
}
}
这里用到的注解和继承可以先忽视,3,4中会做解释
Context上下文类
这个类的核心就是存储
map
所有的策略类,然后提供getHandler()
方法,供外界匹配获取对应策略类执行相关逻辑那么这个map是如何存储的呢?我在实践中遇到是通过
Map<TypeEnum,Handler>
也就是定义枚举类作为键,存储其对应枚举类此处有一个可拓展的点就是这个map是如何封装的,我遇到的有这么两种:
- 在Context中将所有策略类定义为属性,然后@Resourse注入
- 通过注解的方式进行解耦
第一种自动注入+switch的方式
@Component
public class ProductTypeHandlerContext {
@Resource
private CommonHandler commonHandler;
@Resource
privateRecorderHandler recorderHandler;
//...其他策略类
public ProductTypeHandler getHandler(EnumProductType emumType) {
ProductTypeHandler handler = null;
switch (EnumProductType) {
case COMMANDER:
handler = commonHandler;
break;
case RECORDER:
handler = recorderHandler;
break;
default:
throw new BizException("未匹配到目标类型");
break;
}
return handler;
}
}
可以看到第一种方案比较轻量级,下面来着重举例看看第二种情况:
@Component
public class HandlerContext{
private static final Map<EnumProductType, ProductTypeHandler> handlerMap = new HashMap<>();
@Autowired
private HandlerContext(List<ProductTypeHandler> handlers) {
handlers.forEach(processor -> {
//这一段是通过反射获取到策略类
long count = new ArrayList<>(Arrays.asList(processor.getClass().getInterfaces())).stream().filter(m -> m.getName().equals("org.springframework.aop.framework.Advised")).count();
try {
Class<?> ifaceClass = (Class) (count > 0 ? processor.getClass().getMethod("getTargetClass").invoke(processor) : processor.getClass());
this.handlerMap.put(ifaceClass.getAnnotation(ProductTypeAssembleTag.class).TAG(), processor);
} catch (Exception e) {
log.error("HandlerContext初始化失败",e);
throw new BizException("HandlerContext初始化失败");
}
});
}
public ProductTypeHandler getHandler(EnumProductType emumType) {
return handlerMap.get(emumType);
}
}
可以看到Context主要分为三部分:
- map
- 构造函数–>初始化map
- getHandler()
这里的重点在2中的HandlerContext()
构造方法,通过@Autowired
注解,Spring会将所有继承了策略接口ProductTypeHandler
的策略类注入为列表作为参数,那么在方法内部对列表中的策略类依次进行map.put就可以了
那如何将策略类对应到其所属的EnumType上呢?这里通过自定义一个注解,为其添加一个名为Tag的EnumType属性,在每个策略类上面加上这个注解,就大功告成了,这也是上面的策略类的注解来源
/**
* 注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ProductTypeAssembleTag {
EnumsLogin.EnumProductType TAG();
}
至于HandlerContext()中的try中所涉及的逻辑,是检查当前的 processor
类是否是一个 Spring AOP 代理类:
"org.springframework.aop.framework.Advised"
接口是 Spring AOP 代理类的标志。如果 count > 0
,说明 processor
是一个 AOP 代理类。
如果 processor
是 AOP 代理类,则调用 processor.getClass().getMethod("getTargetClass").invoke(processor)
获取目标类的 Class
对象,这样可以获取到代理类的实际目标类(即被代理的类)。如果 processor
不是代理类,则直接使用 processor.getClass()
。
getTargetClass()
方法是 AOP 代理类的一个方法,它返回代理对象的实际目标类(即未被代理的原始类)。
进一步思考
如果策略类a,b的执行逻辑是一样的,那么要写重复的代码吗?
这时候就可以回归到最基础的继承上,通过定义一个CommonHandler,在其中写一次默认逻辑,然后其他策略类进行继承这也是上面的策略类有继承的原因
public class CommonHandler {
public void assemble(ResponseBO resBO) {
log.info("默认处理--目前没有特殊处理逻辑");
}
}
至此,一个通过注解和继承优化的策略类实现方案就得以使用,在调用类通过注入HandlerContext,调用getHandler(enumType).assemble就可以传入枚举类获取对应策略类执行对应逻辑
总结
针对Context中map的两种初始化方案:
方案 | 优点 | 缺点 |
---|---|---|
自动注入+switch | - 简单直观,易于理解和维护 - 性能较好,避免了反射开销 | - 扩展性差,需要修改 switch 语句才能添加新策略- 高耦合,修改策略类需要修改 Context 类 |
注解解耦 | - 解耦性强,符合开闭原则 - 易于扩展,不需要修改现有代码 - 支持 AOP 代理 | - 性能开销较高,使用了反射 – 启动时的反射性能开销 |