专栏名称: 芋道源码
纯 Java 源码分享公众号,目前有「Dubbo」「SpringCloud」「Java 并发」「RocketMQ」「Sharding-JDBC」「MyCAT」「Elastic-Job」「SkyWalking」「Spring」等等
目录
相关文章推荐
芋道源码  ·  工作中如何体现一个人的技术深度? ·  2 天前  
芋道源码  ·  2024年,只有搞颜色的 P 站真正关心网站性能 ·  3 天前  
芋道源码  ·  又来了一款神级搜索引擎,差别真的挺大! ·  5 天前  
芋道源码  ·  Redission 分布式锁原理分析 ·  6 天前  
51好读  ›  专栏  ›  芋道源码

复杂业务下,如何优雅的使用设计模式来优化代码?

芋道源码  · 公众号  · Java  · 2024-11-27 09:30

正文

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入芋道快速开发平台知识星球。下面是星球提供的部分资料: 

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号、CRM 等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本 

来源:juejin.cn/post/
7291937079875928103


1、引言

本文以一个实际案例来介绍在解决业务需求的路上,如何通过常用的设计模式来逐级优化我们的代码,以把我们所了解的到设计模式真实的应用于实战。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

2、背景

假定我们现在有一个订单流程管理系统,这个系统对于用户发起的一笔订单,需要你编写代码按照以下环节进行依次处理

注:本文不会对每个环节的实现细节进行描述,读者也不必了解这每个环节的实现,我们只需要关注代码架构设计

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

3、第一次迭代

按照背景,我们如果不是打算if-else一撸到底的话,我们最合适使用的设计模式应该是责任链模式,于是我们先打算用责任链模式来做我们的第一次迭代。

先整体看下类图:

我们定义一个抽象类,抽象类中定义了下一个处理器,方便后期我们读取配置直接构建责任链处理顺序:

@Data
public abstract class BizOrderHandler {

    /**
     * 下一个处理器
     */

    private BizOrderHandler nextBizOrderHandler;

    /**
     * 处理器执行方法
     * @param param 责任链参数
     * @return 执行结果
     */

    public abstract Result handle(ProductVO param);


    /**
     * 链路传递
     * @param param
     * @return
     */

    protected Result next(ProductVO param) {
        //下一个链路没有处理器了,直接返回
        if (Objects.isNull(nextBizOrderHandler)) {
            return Result.success();
        }
        //执行下一个处理器
        return nextBizOrderHandler.handle(param);
    }

}

然后我们将需要实现的流程都来实现这个接口 (为了简单只列举一个)

public class StorageCheckBizOrderHandler extends BizOrderHandler {
    @Override
    public Result handle(ProductVO param) {
        // 这里写上仓储管理的业务逻辑
        System.out.println("StorageCheckBizOrderHandler doing business!");
        return super.next(param);
    }
}

通过调用父类的next方法实现了链式传递,接下来我们就可以使用责任链来实现业务了

public class OrderHandleCases {

    static Map handlerMap = new HashMap<>();

    static {
        handlerMap.put("Storage"new StorageCheckBizOrderHandler());
        handlerMap.put("Payment"new PaymentBizOrderHandler());
        handlerMap.put("RightCenter"new RightCenterBizOrderHandler());
        handlerMap.put("PointDeduction"new PointDeductBizOrderHandler());
    }

    public static void main(String[] args) {
        // 这里可以从nacos配置中心读取责任链配置
        BizOrderHandler handler1 = initHandler(Lists.newArrayList("Storage""RightCenter""PointDeduction","Payment"));
        // 虚拟化一个产品订单
        ProductVO productVO = ProductVO.builder().build();
        Result result = handler1.handle(productVO);
        System.out.println("订单处理 成功");
    }


    /**
     * 根据责任链配置构建责任链
     * @param handlerNameChain 责任链执行顺序
     * @return 首个处理器
     */

    private static BizOrderHandler initHandler(List handlerNameChain) {
        List handlers = new ArrayList<>();
        for (int i = 0; i             String cur = handlerNameChain.get(i);
            String next = i + 1 1)) : null;
            BizOrderHandler handler = handlerMap.get(cur);
            if (next != null) {
                handler.setNextBizOrderHandler(handlerMap.get(next));
            }
            handlers.add(handler);
        }
        return handlers.get(0);
    }

}

上面的代码中通过initHandler这个方法来组建整个责任链条,还能实现动态配置,比如后期需要撤掉积分模块的商品处理,改个配置就行,感觉责任链完美搞定了这个问题,第一版就这样开心上线。

4、第二次迭代

产品又来了,提了一个新的需求

  • 产品说,我们需要支持多租户,每种租户的订单流程都是不一样的
  • 租户A:仓储检查->权益扣减->积分扣减->剩余金额支付
  • 租户B:仓储检查->积分扣减->权益扣减

也就是说现在流程变成这样:

来了多租户,这有何难,再加一条责任链配置不就好了,直接写代码如下:

public class OrderHandleCases {

    static Map handlerMap = new HashMap<>();

    static {
        handlerMap.put("Storage"new StorageCheckBizOrderHandler());
        handlerMap.put("Payment"new PaymentBizOrderHandler());
        handlerMap.put("RightCenter"new RightCenterBizOrderHandler());
        handlerMap.put("PointDeduction"new PointDeductBizOrderHandler());
    }

    public static void main(String[] args) {
        // 租户A的责任链
        BizOrderHandler handler1 = initHandler(Lists.newArrayList("Storage""RightCenter""PointDeduction""Payment"));
        ProductVO productVO = ProductVO.builder().build();
        Result result1 = handler1.handle(productVO);
        // 租户B的责任链
        BizOrderHandler handler2 = initHandler(Lists.newArrayList("Storage""PointDeduction""RightCenter"));
        Result result2 = handler2.handle(productVO);
        System.out.println("订单处理 成功");

    }

    /**
     * 根据责任链配置构建责任链
     * @param handlerNameChain 责任链执行顺序
     * @return 首个处理器
     */

    private static BizOrderHandler initHandler(List handlerNameChain) {
        List handlers = new ArrayList<>();
        for (int i = 0; i             String cur = handlerNameChain.get(i);
            String next = i + 1 1)) : null;
            BizOrderHandler handler = handlerMap.get(cur);
            if (next != null) {
                handler.setNextBizOrderHandler(handlerMap.get(next));
            }
            handlers.add(handler);
        }
        return handlers.get(0);
    }

}

上面的代码相比之前的就是多加了一条新的租户B的责任链配置。感觉这个功能不就实现了嘛 结果一运行:堆栈溢出!

咋回事 怎么堆栈溢出了,咱们仔细看一下 发现咱们的Map里面存放的实例全部是单例,搞出来了环形链表了....

上图中黑色箭头代表第一个租户的流程,绿色箭头代表第二个租户,第二个租户的流程在执行到权益扣减环节,后面由于第一个租户配置的下一个环节是积分扣减,于是在这里形成了环。

看来单例不行,咱们得搞多例 既然需要多次构建对象,于是咱们搬出来下一个设计模式“简单工厂模式”:

public class BizOrderHandlerFactory {

    public static BizOrderHandler buildBizOrderHandler(String bizType) {
        switch (bizType) {
            case "Storage":
                return new StorageCheckBizOrderHandler();
            case "Payment":
                return new PaymentBizOrderHandler();
            case "RightCenter":
                return new RightCenterBizOrderHandler();
            case "PointDeduction":
                return new PointDeductBizOrderHandler();
            default:
                return null;
        }
    }

}

然后我们改写initHandler方法,不再从map中取实例,转为从工厂方法里面获取实例:

private static BizOrderHandler initHandlerPro(List handlerNameChain) {
        List handlers = new ArrayList<>();
        for (String s : handlerNameChain) {
        BizOrderHandler handler = BizOrderHandlerFactory.buildBizOrderHandler(s);
        handlers.add(handler);
        }
        for (int i = 0; i         BizOrderHandler handler = handlers.get(i);
        BizOrderHandler nextHandler = i + 1 1) : null;
        handler.setNextBizOrderHandler(nextHandler);
        }
        return handlers.get(0);
        }

public static void main(String[] args) {
        BizOrderHandler handler1 = initHandlerPro(Lists.newArrayList("Storage""Payment""RightCenter""PointDeduction"));
        BizOrderHandler handler2 = initHandlerPro(Lists.newArrayList("Storage""RightCenter""PointDeduction"));
        ProductVO productVO = ProductVO.builder().build();
        Result result = handler1.handle(productVO);
        System.out.println("订单处理 成功--租户1");
        result = handler2.handle(productVO);
        System.out.println("订单处理 成功--租户2");
        }

执行代码:

好了问题完美解决,现在多租户也支持了。上线搞定这次需求

5、第三次迭代

产品又又来了,提了一个新的需求

产品说,我们需要支持条件判断,租户A要求,权益扣减和积分扣减必须全部成功完成一个就可以进入支付环节,不必都要把权益扣减和积分扣减流程走一遍

分析一下这个需求权益扣减和积分扣减都要完成才可以进入支付环节 当然最简单的改法是在权益和积分环节做个判断,要是失败了就跳出责任链,但是假如产品经理下次又说权益扣减和积分扣减完成一个就能进入支付,我们还得修改这个权益和积分实现里面的判断,频繁修改实现可并不是好事。

那咱们可以考虑代理模式,熟悉网关的都知道网关其实就是一个大代理,咱们按照这种思想可以搞一个网关代理权益扣减和积分扣减环节。于是咱们搞出来一个“网关”组件

@Data
public class BizOrderHandlerUnionGateway extends BizOrderHandler {
    List proxyHandlers;

    @Override
    public Result handle(ProductVO param) {
        boolean isAllSuccess = true;
        if (proxyHandlers != null) {
            for (BizOrderHandler handler : proxyHandlers) {
                Result result = handler.handle(param);
                if (result.isSuccess()) {
                    // 一个代理执行器 执行成功 则继续执行
                    continue;
                } else {
                    isAllSuccess = false;
                    break;
                }
            }
        }
        if (isAllSuccess) {
            return super.next(param);
        }else{
            throw new RuntimeException("execute Failed");
        }
    }
}

上面的网关叫做union网关也就是并集网关,也就是说代理的处理器全部都执行成功才继续传递责任链,需要注意的是这个类也是BizOrderHandler的一个实现,只不过它的内部没有逻辑,只是对proxyHandlers中的组件进行代理。

然后简单修改下工厂 加一个分支:

public static BizOrderHandler buildBizOrderHandler(String bizType) {
        switch (bizType) {
        case "Storage":
        return new StorageCheckBizOrderHandler();
        case "Payment":
        return new PaymentBizOrderHandler();
        case "RightCenter":
        return new RightCenterBizOrderHandler();
        case "PointDeduction":
        return new PointDeductBizOrderHandler();
        case "UnionGateway":
        return new BizOrderHandlerUnionGateway();
default:
        return null;
        }
        }

然后我们用下面的方法获取首个执行节点,就可以执行整个责任链了:

private static BizOrderHandler initHandlerWithGateway() {
        BizOrderHandler storage = BizOrderHandlerFactory.buildBizOrderHandler("Storage");
        BizOrderHandler payment = BizOrderHandlerFactory.buildBizOrderHandler("Payment");
        BizOrderHandler rightCenter = BizOrderHandlerFactory.buildBizOrderHandler("RightCenter");
        BizOrderHandler pointDeduction = BizOrderHandlerFactory.buildBizOrderHandler("PointDeduction");
        BizOrderHandlerUnionGateway unionGateway = (BizOrderHandlerUnionGateway) BizOrderHandlerFactory.buildBizOrderHandler("UnionGateway");
        storage.setNextBizOrderHandler(unionGateway);
        unionGateway.setNextBizOrderHandler(payment);
        // unionGateway 加入责任链,权益和积分交给这个uniongateway进行代理控制
        unionGateway.setProxyHandlers(Lists.newArrayList(rightCenter, pointDeduction));
        return storage;
        }

6、第四次迭代

产品又又又来了,这次提了一个技术需求

用户反馈生产订单流接口响应过慢,页面卡顿,观察接口发现目前的订单流程需要走的链路比较冗长,虽然用了责任链模式但本质上代码执行仍然是同步的,导致一个订单流完成耗费的时间过长,现在希望订单流接口异步化,然后需要发挥分布式部署的优势,每一个环节可以单独分散到每个单个部署节点上执行。

这次我们发现问题需要异步化还要分布式,这怎么办,显然简单的内存责任链不行了,咱们得上升到分布式责任链模式的方式,那怎么实现分布式责任链呢,咱们可以借助MQ来实现消息触发,于是观察者模式上线,这次咱们借助观察者模式的思想彻底完成分布式重构。

ps:果然需求演进的最后就是重构,不重构没有KPI。

咱们首先定义一个事件,这个就是订单流事件:

@Data
public class OrderFlowEvent implements Serializable {

    private String orderNo;

    private String currentFlow;

    private String nextFlow;

}

这个事件可以在订单流发起的时候丢到消息队列里面,然后就可以进行订单流的流转了,下面我们来看消息处理逻辑,咱们使用模板方法再次进行一次代码优化,这里还是一个抽象类,然后我们的,支付、权益、积分只需要实现这个抽象类实现handleEvent逻辑就可以了,此外我们只用一个Topic,当前环节处理完成之后如果还有后续流程则再次发送消息到消息队列,进行下一步处理,此外handlerMap 代表责任链名称和责任链处理器的对应关系,handlerChain则是责任链的环节配置。

@Data
public abstract class BizHandler {

    String topicName = "biz_handle_topic";

    Map handlerMap = new HashMap<>();

    Map handlerChain = new LinkedHashMap<>();

    /**
     * 模板方法:在收到订单流的消息之后将进到这里进行业务逻辑处理
     *
     * @param msg 订单流消息
     */

    public void handle(String msg) {
        if (CollectionUtils.isEmpty(handlerMap) || CollectionUtils.isEmpty(handlerChain)) {
            //log.warn("handlerMap or handlerChain is empty");
            return;
        }
        OrderFlowEvent orderFlowEvent = JSON.parseObject(msg, OrderFlowEvent.class);
        String currentFlow = orderFlowEvent.getCurrentFlow();
        String nextFlow = handlerChain.get(currentFlow);
        // 当前环节的处理器进行业务处理
        Result result = handlerMap.get(currentFlow).handleEvent(orderFlowEvent);
        if (!result.isSuccess()) {
            throw new RuntimeException("handleException");
        }
        if (nextFlow == null) {
            return;
        }
        if (result.isSuccess()) {
            // 处理成功并且还有后续流程则再次向订单流Topic中发送消息
            sendFlowMsg(result.getData(), nextFlow, handlerChain.get(nextFlow));
        }
    }

    public abstract Result handleEvent(OrderFlowEvent orderFlowEvent);

    public void sendFlowMsg(Object data, String currentFlow, String nextFlow) {
        OrderFlowEvent orderFlowEvent = new OrderFlowEvent();
        orderFlowEvent.setCurrentFlow(currentFlow);
        orderFlowEvent.setNextFlow(nextFlow);
        MqUtils.sendMsg(topicName, JSON.toJSONString(orderFlowEvent));
    }


}

例如仓储环节可以这样实现:

public class StorageBizHandler extends BizHandler {

    @Override
    public Result handleEvent(OrderFlowEvent orderFlowEvent) {
        System.out.println("StorageBizHandler handle orderFlowEvent=" + JSON.toJSONString(orderFlowEvent));
        return Result.success();
    }
}

使用的时候则可以这样:

public class OrderFlowClient {

    void handleOrder() {
        // 定义一下流程名称和流程实例的对应关系
        Map handlerMap = new HashMap<>();
        handlerMap.put("Storage"new StorageBizHandler());
        handlerMap.put("PointDeduction"new PointDeductionBizHandler());
        handlerMap.put("Payment"new PaymentBizHandler());

        //注意这里用LinkedHashMap 保证顺序 key标识当前流程 value标识下一个流程
        Map handlerChain = new LinkedHashMap<>();
        handlerChain.put("Storage""PointDeduction");
        handlerChain.put("PointDeduction""Payment");
        handlerChain.put("Payment"null);

        // 开启分布式订单流转
        Map.Entry first = handlerChain.entrySet().iterator().next();
        String key = first.getKey();
        OrderFlowEvent orderFlowEvent = new OrderFlowEvent();
        orderFlowEvent.setCurrentFlow("Storage");
        orderFlowEvent.setOrderNo("order001123124123");
        handlerMap.get(key).handle(JSON.toJSONString(orderFlowEvent));
    }

}

最后咱们完成了这次大的技术演进需求,但是就到此结束了吗?按照这种设计思路改动之后你发现分布式环境下各种并发问题又出现了,于是你还需要分布式锁来控制,有了分布式锁你发现环节失败了还得引入重试逻辑,重试应该怎么设计,所以发现到了分布式系统下问题变得复杂了,还得继续想办法一个个攻克。

6、总结

本文通过一次简单的需求演进分别讲述了责任链、模板方法、策略模式、工厂模式、代理模式、观察者模式的使用,通过实际场景介绍下不同需求下如何通过适合的设计模式来解决问题。


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)