复杂业务下如何使用良好的设计模式服务多变的需求

1、引言

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

2、背景

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

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

3、第一次迭代

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

先整体看下类图:

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

java 复制代码
@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);
    }

}

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

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

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

java 复制代码
public class OrderHandleCases {

    static Map<String, BizOrderHandler> 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<String> handlerNameChain) {
        List<BizOrderHandler> handlers = new ArrayList<>();
        for (int i = 0; i < handlerNameChain.size(); i++) {
            String cur = handlerNameChain.get(i);
            String next = i + 1 < handlerNameChain.size() ? handlerNameChain.get((i + 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:仓储检查->积分扣减->权益扣减

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

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

java 复制代码
public class OrderHandleCases {

    static Map<String, BizOrderHandler> 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<String> handlerNameChain) {
        List<BizOrderHandler> handlers = new ArrayList<>();
        for (int i = 0; i < handlerNameChain.size(); i++) {
            String cur = handlerNameChain.get(i);
            String next = i + 1 < handlerNameChain.size() ? handlerNameChain.get((i + 1)) : null;
            BizOrderHandler handler = handlerMap.get(cur);
            if (next != null) {
                handler.setNextBizOrderHandler(handlerMap.get(next));
            }
            handlers.add(handler);
        }
        return handlers.get(0);
    }

}

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

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

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

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

java 复制代码
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中取实例,转为从工厂方法里面获取实例:

java 复制代码
private static BizOrderHandler initHandlerPro(List<String> handlerNameChain) {
    List<BizOrderHandler> handlers = new ArrayList<>();
    for (String s : handlerNameChain) {
        BizOrderHandler handler = BizOrderHandlerFactory.buildBizOrderHandler(s);
        handlers.add(handler);
    }
    for (int i = 0; i < handlers.size(); i++) {
        BizOrderHandler handler = handlers.get(i);
        BizOrderHandler nextHandler = i + 1 < handlerNameChain.size() ? handlers.get(i + 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要求,权益扣减和积分扣减必须全部成功完成一个就可以进入支付环节,不必都要把权益扣减和积分扣减流程走一遍

分析一下这个需求权益扣减和积分扣减都要完成才可以进入支付环节 当然最简单的改法是在权益和积分环节做个判断,要是失败了就跳出责任链,但是假如产品经理下次又说权益扣减和积分扣减完成一个就能进入支付,我们还得修改这个权益和积分实现里面的判断,频繁修改实现可并不是好事。 那咱们可以考虑代理模式,熟悉网关的都知道网关其实就是一个大代理,咱们按照这种思想可以搞一个网关代理权益扣减和积分扣减环节。于是咱们搞出来一个"网关"组件

java 复制代码
@Data
public class BizOrderHandlerUnionGateway extends BizOrderHandler {
    List<BizOrderHandler> proxyHandlers;
    @Override
    public Result handle(ProductVO param) {

        if (proxyHandlers != null) {
            for (BizOrderHandler handler : proxyHandlers) {
                Result result = handler.handle(param);
                if (!result.isSuccess()) {
                    return result;
                } else {
                    throw new RuntimeException(handler.getClass().getSimpleName() + "执行失败,责任链跳出");
                }
            }
        }
        return super.next(param);
    }
}

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

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

java 复制代码
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;
    }
}

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

java 复制代码
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。

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

java 复制代码
@Data
public class OrderFlowEvent implements Serializable {

    private String orderNo;

    private String currentFlow;

    private String nextFlow;

}

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

java 复制代码
@Data
public abstract class BizHandler {

    String topicName = "biz_handle_topic";

    Map<String, BizHandler> handlerMap = new HashMap<>();

    Map<String, String> 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));
    }


}

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

java 复制代码
public class StorageBizHandler extends BizHandler {

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

使用的时候则可以这样:

java 复制代码
public class OrderFlowClient {

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

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

        // 开启分布式订单流转
        Map.Entry<String, String> 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、总结

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

相关推荐
AskHarries42 分钟前
Java字节码增强库ByteBuddy
java·后端
佳佳_1 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平2 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
BiteCode_咬一口代码3 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
齐 飞4 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod4 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。5 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man5 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*5 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu5 小时前
Go语言结构体、方法与接口
开发语言·后端·golang