• 【springboot进阶】摆脱 if/else 的高级应用 - 策略模式


    目录

    一、策略模式的介绍

    二、策略模式的使用场景

    三、策略模式的应用

    1、入参和出参类

    2、策略接口

    3、策略具体实现

    4、策略测试

    三、一些使用技巧

    四、总结


    对于一个逻辑相对复杂的功能应用中,难免需要做很多的逻辑判断,需要写一堆的 if/else,更糟糕的情况是里面还会嵌套在大量的 if/else,如果代码没注释,那简直就让人疯掉了。这时候可能考虑用策略模式去处理一些具有相同逻辑的代码,免除代码体中长串的 if/else 

    一、策略模式的介绍

    可以先看一张类图,看不懂也没关系,笔者当初刚开始接触的时候也是懵的,第三小节会以人话的方式介绍如何应用到实际的问题中,所以同学们可以先有个概念。

    这个模式涉及到三个角色:

      ● 环境(Context)角色:持有一个Strategy的引用。

      ● 抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。

      ● 具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。

    二、策略模式的使用场景

    举个最常见的场景,就是支付方式,一般有支付宝、微信、银行卡等,如果从代码上来看就是以下这样的。

    1. //支付接口
    2. pay(payType, orderNo) {
    3. if (payType == 微信支付) {
    4. 微信支付处理
    5. } else if (payType == 支付宝) {
    6. 支付宝支付处理
    7. } else if (payType == 银行卡) {
    8. 银行卡支付处理
    9. } else {
    10. 暂不支持的支付方式
    11. }
    12. }

    如果以后需要增加新的支付方式,我们还需要改这里的代码,再加一段 else if 代码。但是这样做,其实违背了面向对象的 2 个基本原则:

            ● 单一责任原则:一个类应该只有一个发生变化的原因

            ● 开闭原则:对扩展开放,对修改关闭

    三、策略模式的应用

    我们针对上面支付方式的场景,看看一般策略模式是如何应用到其中的。

    1、入参和出参类

    定义策略接口统一的入参字段和出参字段,这对于所有的支付方式都是一样的。

    1)订单支付类

    入参类,定义了一个支付订单可能需要的字段,这里只是简单列举了一些,其中要注意 channel 字段,这是我们在使用策略中的重点,下面会介绍。

    1. /**
    2. * 订单支付
    3. *
    4. * @Author Liurb
    5. * @Date 2022/11/26
    6. */
    7. @Data
    8. public class PayOrder {
    9. /**
    10. * 支付金额,单位元
    11. */
    12. private String mete;
    13. /**
    14. * 用户手机号
    15. */
    16. private String phone;
    17. /**
    18. * 支付渠道
    19. * ALIPAY:支付宝
    20. * WECHAT:微信
    21. * CARD:银行卡
    22. */
    23. private String channel;
    24. }

    2)支付结果类

    出参类,定义了订单支付后所需返回的字段,这里也是简单列举了一些。

    1. /**
    2. * 支付结果
    3. *
    4. * @Author Liurb
    5. * @Date 2022/11/26
    6. */
    7. @Data
    8. public class PayResult {
    9. /**
    10. * 订单号
    11. */
    12. private String order;
    13. /**
    14. * 支付结果
    15. * 1:成功
    16. */
    17. private Integer code;
    18. }

    2、策略接口

    对应着第一点说到的策略类图,去定义策略中所涉及到的接口,但是笔者这边在实际使用中会有些不同。

    1)支付处理统一入口

    这是暴露给外部业务调用的支付接口。

    1. /**
    2. * 支付处理服务统一入口
    3. *
    4. * @Author Liurb
    5. * @Date 2022/11/26
    6. */
    7. public interface VendorPaymentService {
    8. /**
    9. * 付款
    10. *
    11. * @param payOrder
    12. * @return
    13. */
    14. PayResult pay(PayOrder payOrder);
    15. }

    2)支付处理服务接口

    相当于策略类图中的 Strategy 策略角色。

    1. /**
    2. * 支付处理服务
    3. *
    4. * @Author Liurb
    5. * @Date 2022/11/26
    6. */
    7. public interface PaymentHandleService {
    8. /**
    9. * 付款
    10. *
    11. * @param payOrder
    12. * @return
    13. */
    14. PayResult pay(PayOrder payOrder);
    15. }

    3)支付处理上下文

    简单来说就是选择哪种具体策略角色来处理,可以看到这里的入参是上面说的 channel 字段,出参则是策略后所具体的策略角色接口实现。

    1. /**
    2. * 支付处理上下文
    3. *
    4. * @Author Liurb
    5. * @Date 2022/11/26
    6. */
    7. public interface PaymentContextService {
    8. /**
    9. * 获取处理上下文
    10. *
    11. * @param channel
    12. * @return
    13. */
    14. PaymentHandleService getContext(String channel);
    15. }

    3、策略具体实现

    下面,我们要对上面定义的接口分别写出它们的实现类。

    1)支付处理服务统一入口

    方法中需要持有一个上下文接口,用于根据订单的渠道,获取具体的支付处理类。

    1. /**
    2. * 支付处理服务统一入口
    3. *
    4. * @Author Liurb
    5. * @Date 2022/11/26
    6. */
    7. @Service
    8. public class VendorPaymentServiceImpl implements VendorPaymentService {
    9. @Resource
    10. PaymentContextService paymentContextService;
    11. @Override
    12. public PayResult pay(PayOrder payOrder) {
    13. //获取订单中的渠道
    14. String channel = payOrder.getChannel();
    15. //根据渠道,具体选择所使用的支付处理类
    16. PaymentHandleService handleService = paymentContextService.getContext(channel);
    17. //调用该支付处理类的支付方法
    18. return handleService.pay(payOrder);
    19. }
    20. }

    2)支付处理上下文

    这里是策略的核心部分,根据订单的渠道值去找到对应的具体策略支付类。

    1. /**
    2. * 支付处理上下文
    3. *
    4. * @Author Liurb
    5. * @Date 2022/11/26
    6. */
    7. @Service
    8. public class PaymentContextServiceImpl implements PaymentContextService {
    9. /**
    10. * 自动注入所有具体策略实现类
    11. */
    12. @Resource
    13. List handleServiceList;
    14. /**
    15. * 额外定义一个不支持的渠道支付方式实现类
    16. */
    17. @Resource(name = "NonsupportPaymentHandleServiceImpl")
    18. PaymentHandleService nonsupportService;
    19. @Override
    20. public PaymentHandleService getContext(String channel) {
    21. if (StrUtil.isEmpty(channel)) {
    22. return nonsupportService;
    23. }
    24. //策略实现类上都会打上 Payment 注解,并定义支付方式的值,用于适配订单的渠道值
    25. PaymentHandleService handleService = handleServiceList.stream()
    26. .filter(f -> StrUtil.equals(channel, f.getClass().getAnnotation(Payment.class).value()))
    27. .findFirst()
    28. .orElse(nonsupportService);
    29. return handleService;
    30. }
    31. }

    支付方法注解类

    1. /**
    2. * 支付方式注解
    3. *
    4. * @Author Liurb
    5. * @Date 2022/11/26
    6. */
    7. @Target(ElementType.TYPE)
    8. @Retention(RetentionPolicy.RUNTIME)
    9. @Inherited
    10. public @interface Payment {
    11. String value() default "";
    12. }

    不支持的渠道支付方式实现类

    1. /**
    2. * 不支持的业务处理实现
    3. *
    4. * @Author Liurb
    5. * @Date 2022/11/26
    6. */
    7. @Payment("nonsupport")
    8. @Service("NonsupportPaymentHandleServiceImpl")
    9. public class NonsupportPaymentHandleServiceImpl implements PaymentHandleService {
    10. @Override
    11. public PayResult pay(PayOrder payOrder) {
    12. PayResult result = new PayResult();
    13. result.setCode(-1);
    14. return result;
    15. }
    16. }

    3)支付方式具体策略类

    这里是列举支付宝支付的实现,其他的支付类似,可以看到这里注解 Payment 定义了这个支付的渠道字段为 alipay

    1. /**
    2. * 支付宝支付处理
    3. *
    4. * @Author Liurb
    5. * @Date 2022/11/26
    6. */
    7. @Slf4j
    8. @Payment("alipay")
    9. @Service
    10. public class AlipayPaymentHandleServiceImpl implements PaymentHandleService {
    11. @Override
    12. public PayResult pay(PayOrder payOrder) {
    13. PayResult result = new PayResult();
    14. result.setOrder("alipay_202211261234567890");
    15. result.setCode(1);
    16. log.info("支付宝支付处理 订单信息:{} 支付结果:{}", payOrder, result);
    17. return result;
    18. }
    19. }

    4、策略测试

    我们来写一个单元测试类,这里使用支付宝的方式来支付,看看效果。

    1. @SpringBootTest
    2. class SpringbootAdvanceDemoApplicationTests {
    3. @Resource
    4. VendorPaymentService vendorPaymentService;
    5. @Test
    6. void contextLoads() {
    7. PayOrder payOrder = new PayOrder();
    8. payOrder.setChannel("alipay");
    9. payOrder.setMete("100");
    10. payOrder.setPhone("123456");
    11. PayResult payResult = vendorPaymentService.pay(payOrder);
    12. System.out.println(payResult);
    13. }
    14. }

    在上下文实现类中可以看到,这里已经成功注入了我们的策略实现类。

     根据入参的 channel 值,匹配到了支付宝策略实现类。

     成功的跳转到支付宝策略实现类中。

     接下来尝试调整一下channel的值为 wechat 微信方式,看看效果。

     成功跳转到微信策略实现类中。

    三、一些使用技巧

    很多时候,策略模式还可以嵌套使用,例如上面举例的支付方式,如果我们每种支付上针对一些特定情况可以给用户打折,那么在打折这种策略上面,我们又可以写另外一套策略实现。

    四、总结

    同学们可能也有感觉到,使用策略模式,我们就要增加了好几个接口和类,其实这也是它的一个缺点之一,每增加一种支付方式(策略),就需要增加一个类,所以它复用的可能性是很少的。

    同时,从上面的举例也能看出,策略的入参和出参都是固定的,但是在实际的项目中,往往都是复杂多变的,所以导致入参和出参会多出很多不是共用的字段,这对于后续维护也是一个问题,因为调用方不一定熟知哪些字段是必须的,所以这可能需要有相关的接口文档作为辅助。

    但是对于一个大的系统来说,它可能是更利于维护,毕竟可能一种支付方式本身就是一个复杂的功能,如果大家都在一段代码上面写,那肯定是一种灾难。

  • 相关阅读:
    mapstruct实现各个实体间的类型转换(DTO转BO、BO转Entity)的实践
    MySQL update正在执行中突然断电,数据是否更改成功?
    MySQL标准差和方差函数使用
    CPP 获取数组元素个数的问题
    合宙Air724UG LuatOS-Air LVGL API控件-页面 (Page)
    linux查看远程仓库的分支
    上海亚商投顾:沪指高开低走 钠离子电池、储能概念崛起
    8、程序设计语言与语言处理程序基础
    Kubernetes安装-Ubuntu版
    区间信息维护与查询
  • 原文地址:https://blog.csdn.net/lrb0677/article/details/128051083