要在实际项目中应用策略模式,最好先仔细了解策略模式的定义和相关概念,可参考 设计模式(九):策略模式(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伪代码
public BigDecimal calPrice(BigDecimal orderPrice, String buyerType) {
if (VVIP) {
if (8折价格 > 109 元) {
returen 8折价格 - 20;
}
returen 8折价格;
}
if (VIP) {
return 9 折价格;
}
if (普通用户) {
if( VVIP 或 VIP 刚过期且未使用临时折扣) {
更新临时折扣次数();
return 9折价格;
}
return 原价;
}
return 原价;
}
以上是 if...else....实现的伪代码,有多个判断和嵌套,虽然能实现业务需求,但站在系统设计层面上,可维护性和可扩展性都非常低,不符合设计原则。
策略模式
针对类似这种算法规则,可以相互替换且不相互影响的,可以引入策略模式来提升代码的可维护性和可扩展性。
定义一个抽象接口,计算价格的方法,具体实现由策略子类实现:
public interface UserPayService {
/**
* 计算应付价格
*/
public BigDecimal quote(BigDecimal orderPrice);
}
定义计算各个种会员价格的策略类:
public class VVIPPayService implements UserPayService {
@Override
public BigDecimal quote(BigDecimal orderPrice) {
if (8折价格 > 109 元) {
returen 8折价格 - 20;
}
returen 8折价格;
}
}
public class VIPPayService implements UserPayService {
@Override
public BigDecimal quote(BigDecimal orderPrice) {
return 9 折价格;
}
}
public class NormalPayService implements UserPayService {
@Override
public BigDecimal quote(BigDecimal orderPrice) {
if( VVIP 或 VIP 刚过期且未使用临时折扣) {
更新临时折扣次数();
return 9折价格;
}
return 原价;
}
}
引入了策略之后,可以按照如下方式进行价格计算:
public class Test {
public static void main(String[] args) {
UserPayService strategy = new VVIPPayService();
BigDecimal quote = strategy.quote(300);
System.out.println("VVIP会员应付金额:" + quote.doubleValue());
strategy = new VIPPayService();
quote = strategy.quote(300);
System.out.println("VIP会员应付金额:" + quote.doubleValue());
}
}
上面的示例的实现方式就与 JDK 自带的线程池管理器需要手动指定拒绝策略是一样的。
工厂模式
但在业务系统中,是需要根据用户会员等级来匹配对应的策略,根据用户会员等级标识返回计算策略对象。
public BigDecimal calPrice(BigDecimal orderPrice,String vipType) {
if (vipType == VVIP) {
UserPayService strategy = new VVIPPayService();
return strategy.quote(orderPrice);
}
if (vipType == VIP) {
UserPayService strategy = new VIPPayService();
return strategy.quote(orderPrice);
}
if (vipType == normal) {
UserPayService strategy = new NormalPayService();
return strategy.quote(orderPrice);
}
return 原价;
}
使用 if...else...
或 switch
判断用户会员类型,返回对应的计费策略并计算应付金额。
基于 Spring 框架,通常会把策略类注册为 Spring 容器中的 Bean,需要使用时再人容器中取出该 Bean,而不是在代码中 New 一个实例来使用。
public BigDecimal calPrice(BigDecimal orderPrice, String vipType) {
if (vipType == VVIP) {
UserPayService strategy = SpringContextHolder.getBean(VVIPPayService.class);
return strategy.quote(orderPrice);
}
if (vipType == VIP) {
UserPayService strategy = SpringContextHolder.getBean(VIPPayService.class);
return strategy.quote(orderPrice);
}
if (vipType == normal) {
UserPayService strategy = SpringContextHolder.getBean(NormalPayService.class);
return strategy.quote(orderPrice);
}
return 原价;
}
策略模式的使用,在客户端侧必须理解这些算法的区别,以便选择合适的算法。即在选择策略时仍需要执行分支判断。
结合工厂模式,单例模式,可以将策略类缓存起来,创建一个策略工厂专门用于生产和获取策略类。
public class UserPayServiceStrategyFactory {
private static Map<String,UserPayService> services = new ConcurrentHashMap<String,UserPayService>();
public static UserPayService getByUserType(String type){
return services.get(type);
}
public static void register(String userType,UserPayService userPayService) {
Assert.notNull(userType,"userType can't be null");
services.put(userType,userPayService);
}
}
可以在系统启动时就初化一个各个会员等级计费策略对象实例,或使用单例模式,在每次使用时判断,不存在则创建并缓存,类似于 Spring Bean。调用如下:
public BigDecimal calPrice(BigDecimal orderPrice,User user) {
String vipType = user.getVipType();
UserPayService strategy = UserPayServiceStrategyFactory.getByUserType(vipType);
return strategy.quote(orderPrice);
}
上面的工厂类定义了一个线程安全的 ConcurrentHashMap,用来保存所有的策略类的实例,提供一个 getByUserType 方法,可以根据类型直接获取对应的策略类实例。这个 Map 实际就相当于本地缓存了。
工厂类还定义了一个 Register 方法,借助该方法将策略类的 Bean 添加到 Map 中。
Spring Bean 的注册
借助 Spring 中提供的 InitializingBean
接口,该接口为 Bean 提供了属性初始化后的处理方法 afterPropertiesSet()
。凡是继承该接口的类,在 Bean 的属性初始化后都会执行该方法。
接下来将各个策略类稍作改造即可:
@Service
public class VVIPPayService implements UserPayService,InitializingBean {
@Override
public BigDecimal quote(BigDecimal orderPrice) {
if (8折价格 > 109 元) {
returen 8折价格 - 20;
}
returen 8折价格;
}
@Override
public void afterPropertiesSet() throws Exception {
UserPayServiceStrategyFactory.register("VVIP",this);
}
}
@Service
public class VIPPayService implements UserPayService ,InitializingBean{
@Override
public BigDecimal quote(BigDecimal orderPrice) {
return 9 折价格;
}
@Override
public void afterPropertiesSet() throws Exception {
UserPayServiceStrategyFactory.register("VIP",this);
}
}
@Service
public class NormalPayService implements UserPayService,InitializingBean {
@Override
public BigDecimal quote(BigDecimal orderPrice) {
if( VVIP 或 VIP 刚过期且未使用临时折扣) {
更新临时折扣次数();
return 9折价格;
}
return 原价;
}
@Override
public void afterPropertiesSet() throws Exception {
UserPayServiceStrategyFactory.register("NORMAL",this);
}
}
只需要每一个策略类都实现 InitializingBean
接口,并实现其 afterPropertiesSet
方法,在这个方法中调用 UserPayServiceStrategyFactory.register
即可。
这样,在 Spring 初始化的时候,当创建 VVIPPayService、VIPPayService 和 NormalPayService 的时候,会在 Bean 的属性初始化之后,把这个 Bean 注册到 UserPayServiceStrategyFactory
中的 Map 里。
注解实现策略模式
业务场景
开发支付系统,集成支付宝和微信支付,支付宝和微信有多种支付方式,如, JSAPI,付款码,Native 等方式,客户端传入支付渠道和支付方式,选择正确的支付策略。
注解实现
支付订单实体类
@Data public class PayOrder { /** * 支付渠道 */ private String channel; /** * 支付方式 */ private String payMethod; /** * 订单号 */ private String payNo; /** * 支付 */ private BigDecimal amount; }
定义支付处理接口抽象
public interface PayHandler { void handle(PayOrder payOrder); }
创建支付处理注解,来标识该类是处理何种支付方式的支付单
该注解加了
@Service
注解,具体的支付处理类注册为 Spring 容器中的 Bean。@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Service public @interface PayHandlerType { String payMethod(); }
创建支付处理类:PayService.payService方法中,通过策略(支付方式)决定选择哪一个 PayHandler 去处理支付单。
在 PayService 中,维护了一个 payHandlerMap,它的 key 为支付方式,value 为对应的支付处理器 Handler。通过@Autowired 去初始化 orderHandleMap(示例使用了一个 lambda 表达式,List 转 Map 并去重处理)。
@Service public class PayService { private Map<String, PayHandler> payHandlerMap; @Autowired 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支付:
@PayHandlerType(payMethod = "AliJsapi") public class AliJSAPIPayHandler implements PayHandler { @Override public void handle(PayOrder payOrder) { System.out.println("处理支付宝JSAPI支付"); } }
微信JSAPI支付:
@PayHandlerType(payMethod = "WeChatJsapi") public class WeChatJSAPIPayHandler implements PayHandler { @Override public void handle(PayOrder payOrder) { System.out.println("处理微信JSAPI支付"); } }
总结优化
以上示例还可以引入模板方法模式
,将判断 VIP 类型和调用计费方法定义到抽象模板方法类中做为流程规范。
还有 UserPayServiceStrategyFactory.register 调用的时候,第一个参数需要传一个字符串,可以使用使用枚举,或者在每个策略类中自定义一个 getUserType 方法来进行优化。
如果对策略模式和工厂模式了解的话,上面示例并不是严格意义上面的策略模式和工厂模式。首先,策略模式中重要的 Context 角色在这里没有,没有 Context,也就没有用到组合的方式,而是使用工厂代替了。
对于设计模式的学习,重点是学习其思想,其次是在实际业务中的代码实现。
参考资料
注意:本文归作者所有,未经作者允许,不得转载