设计模式-策略模式

1 背景

对于业务开发来说,业务逻辑的复杂是必然的。随着业务发展,需求只会越来越复杂,为了考虑到各种各样的情况,代码中不可避免的会出现很多 if-else。一旦代码中 if-else 过多,就会大大的影响其可读性和可维护性。

笔者曾经看到过一个支付的核心应用,这个应用支持了很多业务的线上支付功能,但是每个业务都有很多定制的需求,所以很多核心的代码中都有一大坨 if-else。每个新业务需要定制的时候,都把自己的 if 放到整个方法的最前面,以保证自己的逻辑可以正常执行。这种做法,后果可想而知。

其实,if-else 是有办法可以消除掉的,其中比较典型的并且使用广泛的就是借助策略模式和工厂模式,准确的说是利用这两个设计模式的思想,彻底消灭代码中的 if-else。本文就结合这两种设计模式,介绍如何消除 if-else,并且,还会介绍如何和 Spring 框架结合,这样读者看完本文之后就可以立即应用到自己的项目中。

2 恶心的 if-else

假设我们要做一个外卖平台,有这样的需求:

  1. 外卖平台上的某家店铺为了促销,设置了多种会员优惠(意味着很多if else 分支),其中包含超级会员折扣 8 折、普通会员折扣 9 折和普通用户没有折扣三种。希望用户在付款的时候,根据用户的会员等级,就可以知道用户符合哪种折扣策略,进而进行打折,计算出应付金额。
  2. 随着业务发展,新的需求要求专属会员要在店铺下单金额大于 30 元的时候才可以享受优惠。
  3. 接着,又有一个变态的需求,如果用户的超级会员已经到期了,并且到期时间在一周内,那么就对用户的单笔订单按照超级会员进行折扣,并在收银台进行强提醒,引导用户再次开通会员,而且折扣只进行一次。

那么,我们可以看到以下伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public BigDecimal calPrice(BigDecimal orderPrice, String buyerType) {
if (用户是专属会员) {
if (订单金额大于30元) {
returen 7折价格;
}
}

if (用户是超级会员) {
return 8折价格;
}

if (用户是普通会员) {
if(该用户超级会员刚过期并且尚未使用过临时折扣){
临时折扣使用次数更新();
returen 8折价格;
}
return 9折价格;
}
return 原价;
}

以上,就是对于这个需求的一段价格计算逻辑,使用伪代码都这么复杂,如果是真的写代码,那复杂度可想而知。

这样的代码中,有很多 if-else,并且还有很多的 if-else 的嵌套,无论是可读性还是可维护性都非常低。那么,如何改善呢?

3 策略模式

3.1 概述

接下来,我们尝试引入策略模式来提升代码的可维护性和可读性。

3.2 策略接口

1
2
3
4
5
6
7
8
9
10
/**
* @author mhcoding
*/
public interface UserPayService {

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

3.3 不同策略的实现类

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
/**
* @author mhcoding
*/
public class ParticularlyVipPayService implements UserPayService {

@Override
public BigDecimal quote(BigDecimal orderPrice) {
if (消费金额大于30元) {
return 7折价格;
}
}
}

public class SuperVipPayService implements UserPayService {

@Override
public BigDecimal quote(BigDecimal orderPrice) {
return 8折价格;
}
}

public class VipPayService implements UserPayService {

@Override
public BigDecimal quote(BigDecimal orderPrice) {
if(该用户超级会员刚过期并且尚未使用过临时折扣){
临时折扣使用次数更新();
returen 8折价格;
}
return 9折价格;
}
}

3.4 新的计算方式

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author mhcoding
*/
public class Test {

public static void main(String[] args) {
UserPayService strategy = new VipPayService();
BigDecimal quote = strategy.quote(300);
System.out.println("普通会员商品的最终价格为:" + quote.doubleValue());

strategy = new SuperVipPayService();
quote = strategy.quote(300);
System.out.println("超级会员商品的最终价格为:" + quote.doubleValue());
}
}

以上,就是一个例子,可以在代码中 New 出不同的会员的策略类,然后执行对应的计算价格的方法。

但是,真正在代码中使用,比如在一个 Web 项目中使用,上面这个 Demo 根本没办法直接用,理由如下:

  • 首先,在 Web 项目中,上面我们创建出来的这些策略类都是被 Spring 托管的,我们不会自己去 New 一个实例出来。
  • 其次,在 Web 项目中,如果真要计算价格,也是要事先知道用户的会员等级,比如从数据库中查出会员等级,然后根据等级获取不同的策略类执行计算价格方法。

那么,Web 项目中真正的计算价格的话,伪代码应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @author mhcoding
*/
public BigDecimal calPrice(BigDecimal orderPrice,User user) {

String vipType = user.getVipType();

if (vipType == 专属会员) {
//伪代码:从Spring中获取超级会员的策略对象
UserPayService strategy = Spring.getBean(ParticularlyVipPayService.class);
return strategy.quote(orderPrice);
}

if (vipType == 超级会员) {
UserPayService strategy = Spring.getBean(SuperVipPayService.class);
return strategy.quote(orderPrice);
}

if (vipType == 普通会员) {
UserPayService strategy = Spring.getBean(VipPayService.class);
return strategy.quote(orderPrice);
}
return 原价;
}

通过以上代码,我们发现,代码可维护性和可读性好像是好了一些,但是好像并没有减少 if-else 啊。

具体来说策略模式的使用上还是有一个比较大的缺点的:客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。

也就是说,虽然在计算价格的时候没有 if-else 了,但是选择具体的策略的时候还是不可避免的还是要有一些 if-else。

另外,上面的伪代码中,从 Spring 中获取会员的策略对象我们是伪代码实现的,那么代码到底该如何获取对应的 Bean 呢?

接下来我们看如何借助 Spring 和工厂模式,解决上面这些问题。

4 工厂模式

4.1 工厂类

为了方便我们从 Spring 中获取 UserPayService 的各个策略类,我们创建一个工厂类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author mhcoding
*/
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);
}
}

这个 UserPayServiceStrategyFactory 中定义了一个 Map,用来保存所有的策略类的实例,并提供一个 getByUserType 方法,可以根据类型直接获取对应的类的实例。还有一个 Register 方法,这个后面再讲。

4.2 策略+工厂后的计算方式

有了这个工厂类之后,计算价格的代码即可得到大大的优化:

1
2
3
4
5
6
7
/**
* @author mhcoding
*/
public BigDecimal calPrice(BigDecimal orderPrice,User user) {
UserPayService strategy = UserPayServiceStrategyFactory.getByUserType(user.getVipType());
return strategy.quote(orderPrice);
}

以上代码中,不再需要 if-else 了,拿到用户的 vip 类型之后,直接通过工厂的 getByUserType 方法直接调用就可以了。

通过策略+工厂,我们的代码很大程度的优化了,大大提升了可读性和可维护性。

4.3 策略工厂初始化

4.3.1 概述

但是,上面还遗留了一个问题,那就是UserPayServiceStrategyFactory中用来保存所有的策略类的实例的 Map 是如何被初始化的?各个策略的实例对象如何塞进去的呢?

4.3.2 Spring Bean 的注册

还记得我们前面定义的 UserPayServiceStrategyFactory 中提供了的 Register 方法吗?他就是用来注册策略服务的。接下来,我们就想办法调用 Register 方法,把 Spring 通过 IOC 创建出来的 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
49
50
/**
* @author mhcoding
*/
@Service
public class ParticularlyVipPayService implements UserPayService,InitializingBean {

@Override
public BigDecimal quote(BigDecimal orderPrice) {
if (消费金额大于30元) {
return 7折价格;
}
}

@Override
public void afterPropertiesSet() throws Exception {
UserPayServiceStrategyFactory.register("ParticularlyVip",this);
}
}

@Service
public class SuperVipPayService implements UserPayService ,InitializingBean{

@Override
public BigDecimal quote(BigDecimal orderPrice) {
return 8折价格;
}

@Override
public void afterPropertiesSet() throws Exception {
UserPayServiceStrategyFactory.register("SuperVip",this);
}
}

@Service
public class VipPayService implements UserPayService,InitializingBean {

@Override
public BigDecimal quote(BigDecimal orderPrice) {
if(该用户超级会员刚过期并且尚未使用过临时折扣){
临时折扣使用次数更新();
returen 8折价格;
}
return 9折价格;
}

@Override
public void afterPropertiesSet() throws Exception {
UserPayServiceStrategyFactory.register("Vip",this);
}
}

只需要每一个策略服务的实现类都实现 InitializingBean 接口,并实现其 afterPropertiesSet 方法,在这个方法中调用 UserPayServiceStrategyFactory.register,并传入本类的type名和对象实例即可。

这样,在 Spring 初始化创建 VipPayService、SuperVipPayService 和 ParticularlyVipPayService 的时候,会在 Bean 的属性初始化之后,把这个 Bean 注册到 UserPayServiceStrategyFactory 中。

4.3.3 工厂模式优化

以上代码,其实还是有一些菲方可以优化:

  • 重复代码
    这里面还可以引入模板方法模式进一步精简,这里就不展开了。

  • 参数传递魔法值
    还有就是,UserPayServiceStrategyFactory.register 调用的时候,第一个参数需要传一个字符串,这里的话其实也可以优化掉。

    比如使用枚举,或者在每个策略类中自定义一个 getUserType 方法,各自实现即可。

总结

本文,我们通过策略模式、工厂模式以及 Spring 的InitializingBean,提升了代码的可读性以及可维护性,彻底消灭了一坨 if-else。

文中的这种做法,大家可以立刻尝试起来,这种实践,是我们日常开发中经常用到的,而且还有很多衍生的用法,也都非常好用。有机会后面再介绍。

其实,如果读者们对策略模式和工厂模式了解的话,文中使用的并不是严格意义上面的策略模式和工厂模式。

首先,策略模式中重要的 Context 角色在这里面是没有的,没有 Context,也就没有用到组合的方式,而是使用工厂代替了。

另外,这里面的 UserPayServiceStrategyFactory 其实只是维护了一个 Map,并提供了 Register 和 Get 方法而已,而工厂模式其实是帮忙创建对象的,这里并没有用到。

所以,读者不必纠结于到底是不是真的用了策略模式和工厂模式。而且,这里面也再扩展一句,所谓的 GOF 23 种设计模式,无论从哪本书或者哪个博客看,都是简单的代码示例,但是我们日常开发很多都是基于 Spring 等框架的,根本没办法直接用的。

所以,对于设计模式的学习,重要的是学习其思想,而不是代码实现!!!希望通过这样的文章,读者可以真正的在代码中使用上设计模式。

文章作者: GeYu
文章链接: https://nuistgy.github.io/2023/05/24/设计模式-策略模式/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Yu's Blog