一、背景
在业务开发中,我们难免遇到如下的控制逻辑,有的分支逻辑大,有的小,而在一个项目中,我们通常会考虑把比较核心的多分支逻辑用策略模式进行实现,从而实现良好的可扩展性。
在该文章中,我们试图通过各段代码案例来观察策略模式的多种实现方式,并对比分析其优缺点。
为此,我们先准备一些基础的代码片段
// 优惠券抽象类 - 处理器
public abstract class CouponHandler {
// 通过该方法在子类中实现了与枚举类的关联关系
public abstract CouponTypeEnum getType();
public abstract String deal();
}
/**
* 满减类型优惠券 - 处理器
*/
@Service
public class CashBackHandler extends CouponHandler {
@Override
public CouponTypeEnum getType() {
return CouponTypeEnum.CASH_BACK;
}
@Override
public String deal() {
System.out.println("执行:满减类优惠券处理逻辑");
return "执行结果(满减类)";
}
}
/**
* 折扣类型优惠券 - 处理器
*/
@Service
public class PercentOffHandler extends CouponHandler {
@Override
public CouponTypeEnum getType() {
return CouponTypeEnum.PERCENT_OFF;
}
@Override
public String deal() {
System.out.println("执行:折扣类优惠券处理逻辑");
return "执行结果(折扣类)";
}
}
/**
* 优惠券类型枚举
*/
@Getter
@AllArgsConstructor
public enum CouponTypeEnum {
CASH_BACK("满减"),
PERCENT_OFF("折扣"),
;
private String desc;
}
二、方案
(一)、方案一(多实现)
@Component
public class CouponFactory implements InitializingBean, ApplicationContextAware {
private static final Map<String, CouponHandler> MAP = new ConcurrentHashMap<>();
private ApplicationContext appContext;
@Override
public void afterPropertiesSet() throws Exception {
// 将 Spring 容器中所有的 CouponHandler 注册到 MAP
appContext.getBeansOfType(CouponHandler.class)
.values()
.forEach(handler -> MAP.putIfAbsent(handler.getType().name(), handler));
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
appContext = applicationContext;
}
public String execute(CouponTypeEnum couponTypeEnum) {
// 执行逻辑
CouponHandler couponHandler = MAP.get(couponTypeEnum.name());
if (couponHandler == null) {
return null;
}
return couponHandler.deal();
}
}
- CouponFactory实现InitializingBean, ApplicationContextAware,重写相应的方法;
- 使用@Component或者@Service或@Configuration修饰,将其交给spring容器管理;
- 使用map存储映射关系,为了避免线程安全问题,需采用ConcurrentHashMap(假设有修改map的操作的话也不至于出现线程安全问题);
- 重写setApplicationContext方法,该方法是ApplicationContextAware的接口方法,根据接口名称,可以知道该类是用于感知Spring容器上下文的,重写该方法,获取applicationContext上下文对象,方便我们上面的CouponFactory类感知到applicationContext的存在,然后通过这个上下文对象进行一些别的操作,例如上面的getBeansOfType方法; 【注:Spring中所有继承Aware类的,都是为了让对象感知容器,因为在Spring中,容器可以很方便感知到对象,但有时候我们需要在对象中感知容器】
- 重写afterPropertiesSet方法,该方法是InitializingBean的接口方法,用于在类初始化(参考类初始化过程)过程中,在属性设置完成之后进行的一些操作。也就是在类初始化过程的最后初始化阶段,完成了静态变量赋值过程之后,进行的一些切入操作;
- 其他类使用时,注入CouponFactory,调用其execute并传入枚举值即可。
(二)方案二(单实现)
@Component
public class CouponFactory implements ApplicationContextAware {
private static final Map<String, CouponHandler> MAP = new ConcurrentHashMap<>();
@Override
public void setApplicationContext(ApplicationContext appContext) throws BeansException {
appContext.getBeansOfType(CouponHandler.class)
.values()
.forEach(handler -> MAP.putIfAbsent(handler.getType().name(), handler));
}
public String execute(CouponTypeEnum couponTypeEnum) {
// 执行逻辑
CouponHandler couponHandler = MAP.get(couponTypeEnum.name());
if (couponHandler == null) {
return null;
}
return couponHandler.deal();
}
}
- 在方案一的基础上,我们减少了一个实现类;
- 不再把ApplicationContext作为CouponFactory的属性;
- 和方案一一样,我们通过getBeansOfType从Spring容器中获取优惠券处理器抽象类CouponHandler的子类实例与枚举建立关系;
(三)方案三(无实现、注解)
@Component
public class CouponFactory {
@Resource
private Map<String, CouponHandler> map;
private static final Map<String, CouponHandler> H_MAP = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
// map无需判空,如果CouponHandler没有实现类,启动就会报错,虽然为非受检异常,但是这个在研发过程中就需要编写抽象类的子类
map.values().forEach(handler -> H_MAP.putIfAbsent(handler.getType().name(), handler));
}
public String execute(CouponTypeEnum couponTypeEnum) {
// 执行逻辑
CouponHandler couponHandler = H_MAP.get(couponTypeEnum.name());
if (couponHandler == null) {
return null;
}
return couponHandler.deal();
}
}
- 在这个方案中,我们不实现任何接口,也不继承任何抽象类
- 使用了@PostConstruct注解,依据该注解完成CouponFactory中优惠券处理器类的map构建
- @PostConstruct注解属于java,与Spring框架进行分离
- 该注解实际是在对象初始化完成后利用反射调用该init()方法来实现一定的逻辑
- @PostConstruct注解的逻辑为什么不放在CouponFactory类的构造方法中?因为在构造方法对@Autowaired修饰的属性进行赋值并不是一个理想的操作,当@Autowaired修饰时,@Autowaired的属性会在构造方法之后才执行,会把编写的逻辑覆盖,而@PostConstruct会在@Autowaired之后才执行
(四)方案四(无实现、无注解)
/**
* 工厂类
*/
@Component
public class CouponFactory {
@Resource
private Map<String, CouponHandler> map;
public String execute(CouponTypeEnum couponTypeEnum) {
// 首字母转小写
char[] chars = couponTypeEnum.getHandler().toCharArray();
chars[0] += 32;
// 获取处理器
CouponHandler couponHandler = map.get(String.valueOf(chars));
// 执行逻辑
return couponHandler.deal();
}
}
使用这个方案需要修改一下枚举类,并且可以将处理器的getType方法删除,如下
/**
* 优惠券类型枚举
*/
@Getter
@AllArgsConstructor
public enum CouponTypeEnum {
CASH_BACK("满减", CashBackHandler.class.getSimpleName()),
PERCENT_OFF("折扣", PercentOffHandler.class.getSimpleName()),
;
private String desc;
private String handler;
}
/**
* 优惠券抽象类
*/
public abstract class CouponHandler {
// 【方案四的这个方法就可以删掉了,因为已经在枚举类里面做了关联】
public abstract CouponTypeEnum getType();
public abstract String deal();
}
- 利用@Resource或者@Autowaired注入map时,会自动获取Spring容器中的子实现类,并赋值,最后map其实是一个LinkHashMap,key为容器中的beanName,value为实例对象;
- 由于key的首字母小写,而枚举值得ClassName首字母为大写,例如key=cashBackService,enum中=CashBackService,因此需要转换一下;
- 为了提高转换效率,可以利用ascii码的规则进行处理,而不采用字符串操作;
- 将计算逻辑放在这里(这里有坑,后面会说到)是因为按照枚举类的使用原则,不应在枚举中放入不该有的逻辑;
(五)方案五(无实现、无注解、减少运算)
方案四由于每次执行execute方法都需要计算一遍,为了减少这样的运算次数,可以考虑放入map中,既可以放入@Resource注入的map中,也可以放入另外的ConcurrentHashMap中。没错,看到这里,其实应该想到在这样的计算过程中,遇到并发,是会有问题的,怎么解决呢?请看方案六
@Component
public class CouponFactory {
@Resource
private Map<String, CouponHandler> map;
public String execute(CouponTypeEnum couponTypeEnum) {
CouponHandler couponHandler = map.get(couponTypeEnum.getHandler());
if (couponHandler == null) {
// 首字母转小写
char[] chars = couponTypeEnum.getHandler().toCharArray();
chars[0] += 32;
// 获取处理器
couponHandler = map.get(String.valueOf(chars));
// 不为空则将首字母大写的key和对应的value放入map
if (couponHandler != null) {
map.putIfAbsent(couponTypeEnum.getHandler(), couponHandler);
}
}
// Assert
if (couponHandler == null) {
return null;
}
// 执行逻辑
return couponHandler.deal();
}
}
@Component
public class CouponFactory {
@Resource
private Map<String, CouponHandler> map;
private static final ConcurrentHashMap<String, CouponHandler> conMap = new ConcurrentHashMap<>();
public String execute(CouponTypeEnum couponTypeEnum) {
CouponHandler couponHandler = conMap.get(couponTypeEnum.getHandler());
if (couponHandler == null) {
// 首字母转小写
char[] chars = couponTypeEnum.getHandler().toCharArray();
chars[0] += 32;
// 获取处理器
couponHandler = map.get(String.valueOf(chars));
// 不为空则将首字母大写的key和对应的value放入conMap
if (couponHandler != null) {
conMap.putIfAbsent(couponTypeEnum.getHandler(), couponHandler);
}
}
// Assert
if (couponHandler == null) {
return null;
}
// 执行逻辑
return couponHandler.deal();
}
}
(六)方案六(无实现、无注解、简化)
在这个方案中,我们需要改改优惠券的策略实现类,如下
/**
* 满减类型优惠券 - 处理器
*/
@Service("CashBackHandler")
public class CashBackHandler extends CouponHandler {
@Override
public String deal() {
System.out.println("执行:满减类优惠券处理逻辑");
return "执行结果(满减类)";
}
}
/**
* 折扣类型优惠券 - 处理器
*/
@Service("PercentOffHandler")
public class PercentOffHandler extends CouponHandler {
@Override
public String deal() {
System.out.println("执行:折扣类优惠券处理逻辑");
return "执行结果(折扣类)";
}
}
/**
* 工厂类
*/
@Component
public class CouponFactory {
@Resource
private Map<String, CouponHandler> map;
public String execute(CouponTypeEnum couponTypeEnum) {
// 删除首字母转小写的逻辑,因为前面@Service加上了value,没有写的情况下首字母会自动转小写,写了的话就会以这个首字母大写的name(id)作为标识
// 获取处理器
CouponHandler couponHandler = map.get(couponTypeEnum.getHandler());
// 执行逻辑
return couponHandler.deal();
}
}
- 对比方案五,我们将计算逻辑全部删除;
- 既然计算逻辑是为了解决从注入的map中的key的首字母是小写的问题,那我们就把key改改,让key也是大写就行了,这样就可以和枚举的值对应上了,因此我们修改@Service的value,这样问题就得到解决了;
- 在这个方案中,我们既不需要采用ConcurrentHashMap这样的有锁数据结构,也不需要担心execute方法的安全问题,毕竟map只在启动的时候写入,后面都是读操作,当然,对于deal()内部的逻辑的线程安全问题是需要考虑的;
总结一下: 1、方案一和方案二都需要实现接口 2、方案三不需要实现接口,而是采用@PostConstruct注解实现,与框架解耦 3、方案四将关联逻辑移到枚举中,同时增加了一丢丢运算,存在并发安全问题 4、方案五为了减少运算次数,增加了逻辑,不仅代码增加了,也未解决并发安全问题 5、方案六在方案四和方案五的基础上,通过修改@Service的value来修改类在容器中的beanID,从而实现无运算下关联关系的建立 6、方案六可能存在由于修改了beanID,导致bean的覆盖问题
综上所述,可以采用方案三或者方案六来实现策略模式。 注:在这些实现方案中,也有用到工厂模式,通过Spring的bean工厂来实现了自己的工厂模式。
有人就问了,为什么不把
@Resource private Map\<String, CouponHandler> map;
放到外面,就不用CouponFactory这个类了?
从实现角度讲是可以的,但是我们搞研发,要的就是一个可扩展,从这个角度,我们如果搞个工厂来实现,不就变成了策略模式+工厂模式吗?这样在面试的时候不就可以多些内容可以吹? 正经点说的话,我们应该这么考虑,当我们使用一个工厂类来实现之后,后面我们就可以很方便地转为抽象工厂模式,一个服务里面,也许不止一个地方会用到策略,那我们就可以升级到抽象工厂模式。这样,扩展性有了,也不用增加很多工厂类,还使得代码具备了灵活性,最重要的是我们多了一个可以用来面试吹水的知识点,何乐而不为?