设计模式(十五):策略模式(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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….实现的伪代码,有多个判断和嵌套,虽然能实现业务需求,但站在系统设计层面上,可维护性和可扩展性都非常低,不符合设计原则。

策略模式

针对类似这种算法规则,可以相互替换且不相互影响的,可以引入策略模式来提升代码的可维护性和可扩展性。

定义一个抽象接口,计算价格的方法,具体实现由策略子类实现:

1
2
3
4
5
6
7
public interface UserPayService {

/**
* 计算应付价格
*/
public BigDecimal quote(BigDecimal orderPrice);
}

定义计算各个种会员价格的策略类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

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 原价;
}
}

引入了策略之后,可以按照如下方式进行价格计算:

1
2
3
4
5
6
7
8
9
10
11
12
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 自带的线程池管理器需要手动指定拒绝策略是一样的。

工厂模式

但在业务系统中,是需要根据用户会员等级来匹配对应的策略,根据用户会员等级标识返回计算策略对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 一个实例来使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 原价;
}

策略模式的使用,在客户端侧必须理解这些算法的区别,以便选择合适的算法。即在选择策略时仍需要执行分支判断。

结合工厂模式单例模式,可以将策略类缓存起来,创建一个策略工厂专门用于生产和获取策略类。

1
2
3
4
5
6
7
8
9
10
11
12
13
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。调用如下:

1
2
3
4
5
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 的属性初始化后都会执行该方法。

接下来将各个策略类稍作改造即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@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 等方式,客户端传入支付渠道和支付方式,选择正确的支付策略。

注解实现

  1. 支付订单实体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Data
    public class PayOrder {

    /**
    * 支付渠道
    */
    private String channel;
    /**
    * 支付方式
    */
    private String payMethod;
    /**
    * 订单号
    */
    private String payNo;
    /**
    * 支付
    */
    private BigDecimal amount;
    }
  2. 定义支付处理接口抽象

    1
    2
    3
    public interface PayHandler {
    void handle(PayOrder payOrder);
    }
  3. 创建支付处理注解,来标识该类是处理何种支付方式的支付单

    该注解加了 @Service 注解,具体的支付处理类注册为 Spring 容器中的 Bean。

    1
    2
    3
    4
    5
    6
    7
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Service
    public @interface PayHandlerType {
    String payMethod();
    }
  4. 创建支付处理类: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
    @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);

    // ...一些后置处理
    }
    }
  5. 支付渠道和支付方式的具体处理类,例如 处理支付宝JSAPI 和 微信JSAPI 的支付。

    支付宝JSAPI支付:

    1
    2
    3
    4
    5
    6
    7
    @PayHandlerType(payMethod = "AliJsapi")
    public class AliJSAPIPayHandler implements PayHandler {
    @Override
    public void handle(PayOrder payOrder) {
    System.out.println("处理支付宝JSAPI支付");
    }
    }

    微信JSAPI支付:

    1
    2
    3
    4
    5
    6
    7
    @PayHandlerType(payMethod = "WeChatJsapi")
    public class WeChatJSAPIPayHandler implements PayHandler {
    @Override
    public void handle(PayOrder payOrder) {
    System.out.println("处理微信JSAPI支付");
    }
    }

总结优化

以上示例还可以引入模板方法模式,将判断 VIP 类型和调用计费方法定义到抽象模板方法类中做为流程规范。

还有 UserPayServiceStrategyFactory.register 调用的时候,第一个参数需要传一个字符串,可以使用使用枚举,或者在每个策略类中自定义一个 getUserType 方法来进行优化。

如果对策略模式和工厂模式了解的话,上面示例并不是严格意义上面的策略模式和工厂模式。首先,策略模式中重要的 Context 角色在这里没有,没有 Context,也就没有用到组合的方式,而是使用工厂代替了。

对于设计模式的学习,重点是学习其思想,其次是在实际业务中的代码实现。

参考资料

  1. 刚来的大神彻底干掉了代码中的if else…
  2. 别再用if-else了,用注解去代替他吧

设计模式(十五):策略模式(Strategy Pattern)实际应用

http://blog.gxitsky.com/2020/12/07/DesignPatterns-15-Strategy-user/

作者

光星

发布于

2020-12-07

更新于

2022-06-17

许可协议

评论