设计模式(十五):策略模式(Strategy Pattern)实际应用
要在实际项目中应用策略模式,最好先仔细了解策略模式的定义和相关概念,可参考 设计模式(九):策略模式(Strategy Pattern)。
策略模式(Strategy Pattern):定义一系列算法(算法家族),并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。
使用了策略模式,在系统设计层面是满足 里氏替换原则 和 开放封闭原则 的,每个算法可以相互替换,在不修改已有算法的情况下易于扩展。
关于if…else
网上和公众号上很多标题党,用干掉 if…else…类的文章标题吸引眼球来描述策略模式如何替换 if...else...
,个人认为如果思路限于替换 if…else… 是比较狭隘的。
查看 Spring 某些源码,也会看到有多层 if…else…嵌套的的使用。if…else… 在一些分支判断处理上是简单直接明了的。稍有个1年以上开发经验都应该不会写一大堆的嵌套 if…else….(超过三层的嵌套),最简单的优化处理是抽出为独立的方法。
要使用设计模式的重点是关注可扩展,可维护。使用策略模式替换 if…else… 的核心是各个算法独立,可以相互替换互不影响。
策略模式应用
业务场景
一个典型的业务场景是电商的会员折扣,节假日促销活动等,要计算不同等级的会员的优惠折扣:
- 电商平台会员有多个等级,为会员设置对应的优惠折扣,其中 VVIP会员 8 折、VIP会员折扣 9 折,普通用户没有折扣三种。
- 系统需要在用户付款时,根据用户的会员等级选择相应的优惠折扣,计算出应付金额。
- 节假日促销,VVIP 会员还可享满 109 减 20 元的优惠。
- 又一个需求,如果VVIP 或 VIP 会员到期且在一周内,且未使用过临时折扣,则按临时折扣只计算一次,并引导用户再次开通会员。
if…else伪代码
1 | public BigDecimal calPrice(BigDecimal orderPrice, String buyerType) { |
以上是 if…else….实现的伪代码,有多个判断和嵌套,虽然能实现业务需求,但站在系统设计层面上,可维护性和可扩展性都非常低,不符合设计原则。
策略模式
针对类似这种算法规则,可以相互替换且不相互影响的,可以引入策略模式来提升代码的可维护性和可扩展性。
定义一个抽象接口,计算价格的方法,具体实现由策略子类实现:
1 | public interface UserPayService { |
定义计算各个种会员价格的策略类:
1 |
|
引入了策略之后,可以按照如下方式进行价格计算:
1 | public class Test { |
上面的示例的实现方式就与 JDK 自带的线程池管理器需要手动指定拒绝策略是一样的。
工厂模式
但在业务系统中,是需要根据用户会员等级来匹配对应的策略,根据用户会员等级标识返回计算策略对象。
1 | public BigDecimal calPrice(BigDecimal orderPrice,String vipType) { |
使用 if...else...
或 switch
判断用户会员类型,返回对应的计费策略并计算应付金额。
基于 Spring 框架,通常会把策略类注册为 Spring 容器中的 Bean,需要使用时再人容器中取出该 Bean,而不是在代码中 New 一个实例来使用。
1 | public BigDecimal calPrice(BigDecimal orderPrice, String vipType) { |
策略模式的使用,在客户端侧必须理解这些算法的区别,以便选择合适的算法。即在选择策略时仍需要执行分支判断。
结合工厂模式,单例模式,可以将策略类缓存起来,创建一个策略工厂专门用于生产和获取策略类。
1 | public class UserPayServiceStrategyFactory { |
可以在系统启动时就初化一个各个会员等级计费策略对象实例,或使用单例模式,在每次使用时判断,不存在则创建并缓存,类似于 Spring Bean。调用如下:
1 | public BigDecimal calPrice(BigDecimal orderPrice,User user) { |
上面的工厂类定义了一个线程安全的 ConcurrentHashMap,用来保存所有的策略类的实例,提供一个 getByUserType 方法,可以根据类型直接获取对应的策略类实例。这个 Map 实际就相当于本地缓存了。
工厂类还定义了一个 Register 方法,借助该方法将策略类的 Bean 添加到 Map 中。
Spring Bean 的注册
借助 Spring 中提供的 InitializingBean
接口,该接口为 Bean 提供了属性初始化后的处理方法 afterPropertiesSet()
。凡是继承该接口的类,在 Bean 的属性初始化后都会执行该方法。
接下来将各个策略类稍作改造即可:
1 |
|
只需要每一个策略类都实现 InitializingBean
接口,并实现其 afterPropertiesSet
方法,在这个方法中调用 UserPayServiceStrategyFactory.register
即可。
这样,在 Spring 初始化的时候,当创建 VVIPPayService、VIPPayService 和 NormalPayService 的时候,会在 Bean 的属性初始化之后,把这个 Bean 注册到 UserPayServiceStrategyFactory
中的 Map 里。
注解实现策略模式
业务场景
开发支付系统,集成支付宝和微信支付,支付宝和微信有多种支付方式,如, JSAPI,付款码,Native 等方式,客户端传入支付渠道和支付方式,选择正确的支付策略。
注解实现
支付订单实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class PayOrder {
/**
* 支付渠道
*/
private String channel;
/**
* 支付方式
*/
private String payMethod;
/**
* 订单号
*/
private String payNo;
/**
* 支付
*/
private BigDecimal amount;
}定义支付处理接口抽象
1
2
3public interface PayHandler {
void handle(PayOrder payOrder);
}创建支付处理注解,来标识该类是处理何种支付方式的支付单
该注解加了
@Service
注解,具体的支付处理类注册为 Spring 容器中的 Bean。1
2
3
4
5
6
7
public PayHandlerType {
String payMethod();
}创建支付处理类:PayService.payService方法中,通过策略(支付方式)决定选择哪一个 PayHandler 去处理支付单。
在 PayService 中,维护了一个 payHandlerMap,它的 key 为支付方式,value 为对应的支付处理器 Handler。通过@Autowired 去初始化 orderHandleMap(示例使用了一个 lambda 表达式,List 转 Map 并去重处理)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class PayService {
private Map<String, PayHandler> payHandlerMap;
public void setPayHandlerMap(List<PayHandler> payHandlers) {
// 注入各种类型的订单处理类
payHandlerMap = payHandlers.stream().collect(
Collectors.toMap(payHandler -> AnnotationUtils.findAnnotation(payHandler.getClass(), PayHandlerType.class).get().payMethod(),
v -> v, (v1, v2) -> v1));
}
public void payService(PayOrder payOrder) {
// ...一些前置处理
// 通过订单来源确定对应的handler
PayHandler payHandler = payHandlerMap.get(payOrder.getPayMethod());
payHandler.handle(payOrder);
// ...一些后置处理
}
}支付渠道和支付方式的具体处理类,例如 处理支付宝JSAPI 和 微信JSAPI 的支付。
支付宝JSAPI支付:
1
2
3
4
5
6
7
public class AliJSAPIPayHandler implements PayHandler {
public void handle(PayOrder payOrder) {
System.out.println("处理支付宝JSAPI支付");
}
}微信JSAPI支付:
1
2
3
4
5
6
7
public class WeChatJSAPIPayHandler implements PayHandler {
public void handle(PayOrder payOrder) {
System.out.println("处理微信JSAPI支付");
}
}
总结优化
以上示例还可以引入模板方法模式
,将判断 VIP 类型和调用计费方法定义到抽象模板方法类中做为流程规范。
还有 UserPayServiceStrategyFactory.register 调用的时候,第一个参数需要传一个字符串,可以使用使用枚举,或者在每个策略类中自定义一个 getUserType 方法来进行优化。
如果对策略模式和工厂模式了解的话,上面示例并不是严格意义上面的策略模式和工厂模式。首先,策略模式中重要的 Context 角色在这里没有,没有 Context,也就没有用到组合的方式,而是使用工厂代替了。
对于设计模式的学习,重点是学习其思想,其次是在实际业务中的代码实现。
参考资料
设计模式(十五):策略模式(Strategy Pattern)实际应用
http://blog.gxitsky.com/2020/12/07/DesignPatterns-15-Strategy-user/