业务设计——责任链验证推翻 if-else 炼狱

news2024/9/20 22:36:30

责任链模式

1. 什么是责任链模式

        在责任链模式中,多个处理器依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条,链条上的每个处理器各自承担各自的处理职责。

image.png

2. 责任链模式优点

        责任链模式的优点在于,它可以动态地添加、删除和调整处理者对象,从而灵活地构建处理链。同时,它也避免了请求发送者和接收者之间的紧耦合,增强了系统的灵活性和可扩展性。


业务中理解责任链

购票请求验证

        在实际购票业务场景中,用户发起一次购票请求后,购票接口在真正完成创建订单和扣减余票行为前,需要验证当前请求中的参数是否正常请求,或者说是否满足购票情况。

  1. 购票请求用户传递的参数是否为空,比如:车次 ID、乘车人、出发站点、到达站点等。
  2. 购票请求用户传递的参数是否正确,比如:车次 ID 是否存在、出发和到达站点是否存在等。
  3. 需要购票的车次是否满足乘车人的数量,也就是列车对应座位的余量是否充足。
  4. 乘客是否已购买当前车次,或者乘客是否已购买当天时间冲突的车次。
  5. 可能实际场景中需要验证的还有很多,就不逐一举例了。

对于完成这些前置校验逻辑,示例代码如下:

public TicketPurchaseRespDTO purchaseTickets(PurchaseTicketReqDTO requestParam) {
	// 购票请求用户传递的参数是否为空
	// 购票请求用户传递的参数是否正确
	// 需要购票的车次是否满足乘车人的数量
	// 乘客是否已购买当前车次,或者乘客是否已购买当天时间冲突的车次
	// ......
}

        解决前置校验需求需要实现一堆逻辑【if-else炼狱】,常常需要写上几百上千行代码。并且,上面的代码不具备开闭原则,以及代码扩展性,整体来说复杂且臃肿。

        为了避免这种坏代码味道【shi山】,我们可以运用责任链设计模式,对购票验证逻辑进行抽象。把每一个if的判断抽象为一个handler过滤器,多个if就化解成了一个过滤器链(职责链),这样子就可以避免在service业务层写太多校验逻辑,让代码更加清爽!并且如果后续还需要新增说明校验的判断,直接新增一个处理器对象就行,无需改动源代码,符合开闭原则


责任链模式重构

下面我们以一个购买商品的业务来搭建责任链架构

1.定义所有过滤链的抽象接口

        为了方便对责任链流程中的任务进行顺序处理,我们需要继承 Spring 框架中的排序接口 Ordered。这将有助于保证责任链中的处理器的顺序执行

public interface AbstractChainHandler <T> extends Ordered { 
    /**
     * 在这个方法里面定义过滤器要干的事儿,每个过滤器链实例通过重写该方法来实现响应的处理逻辑
     * @param requestParam 请求的参数(过滤链要加工校验的对象实际上就是源自于前端传过来的参数)
     */
    void handler(T requestParam);

    /**
     * 一个项目可以有多条过滤链,得通过mark来锁定指定的那条链,也就是说mark就是众多处理器的划分参考
     * @return 一条过滤链组件标识
     */
    String mark();
}

2.定义职责链初始化器和启用函数

这个类中只要做两件事:

  • 项目一启动就把多个过滤器链初始化加载好
  • 准备好调用过滤器链路的总开关函数,我传入一条过滤链的标识mark和请求参数requestParam,你就得给我把这个链路跑起来依次调用我的处理器去校验
public final class AbstractChainContext<T> implements CommandLineRunner { 
                            // CommandLineRunner:SpringBoot 启动完成后执行的回调函数run()

    /**
     * 初始化好的一条条过滤器链都放到集合容器中存好 Key:过滤器链的标识mark   Value:过滤器链中的处理器集合
     */
    private final Map<String, List<AbstractChainHandler>> abstractChainHandlerContainer = new HashMap<>();

    /**
     * 把这个mask对应的链路跑起来依次调用我的处理器去校验
     * @param mark         过滤链组件标识
     * @param requestParam 请求参数
     */
    public void handler(String mark, T requestParam) {
        // 找到那条链子对应的排好序的处理器集合
        List<AbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(mark);
        if (CollectionUtils.isEmpty(abstractChainHandlers)) {
            throw new RuntimeException(String.format("[%s] Chain of Responsibility ID is undefined.", mark));
        }
        // 按序依次调用处理器来进行校验
        abstractChainHandlers.forEach(each -> each.handler(requestParam));
    }

    /**
     * 初始化项目中的所有过滤链
     */
    @Override
    public void run(String... args) throws Exception {
        // 调用 SpirngIOC 工厂获取 AbstractChainHandler 接口类型的 Bean    Key:Bean的名称  Value:处理器实例
        Map<String, AbstractChainHandler> chainFilterMap = ApplicationContextHolder
                .getBeansOfType(AbstractChainHandler.class);
        // chainFilterMap中堆了项目的所有过滤器,下面根据过滤器链的标识mark来进行划分,把划分的各个链子都丢到集合容器中存好
        chainFilterMap.forEach((beanName, bean) -> {
            List<AbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(bean.mark());
            if (CollectionUtils.isEmpty(abstractChainHandlers)) {
                abstractChainHandlers = new ArrayList();
            }
            abstractChainHandlers.add(bean);
            // 注意这里通过Order数值来排序好一条过滤链中的过滤器调用的顺序
            List<AbstractChainHandler> actualAbstractChainHandlers = abstractChainHandlers.stream()
                    .sorted(Comparator.comparing(Ordered::getOrder))
                    .collect(Collectors.toList());
            abstractChainHandlerContainer.put(bean.mark(), actualAbstractChainHandlers);
        });
    }

3.定义一条责任链的抽象

在这里,我们定义一个针对购买操作的责任链的抽象,这样子就可以将处理器实例通过接口进行一个划分

public interface PurchaseFilter <T extends 请求参数的封装类> extends AbstractChainHandler<请求参数的封装类> {

    @Override
    default String mark() {
        // 这个name不是自定义的属性,而是每个枚举类型的名称,默认是1开始,类似于数组下标的东西
        return FilterChainMarkEnum.PURCHASE_FILTER.name();
    }
}

4.定义责任链中的处理器实例

定义查询该商品库存是否满足购买数量的处理器

@Component
public class QueryHandler implements PurchaseFilter {

    @Override
    public void handler(PurchaseTicketReqDTO requestParam) {
        // 校验该商品库存是否满足购买数量操作,不是就抛异常中止责任链校验操作
    }

    @Override
    public int getOrder() {
        return 处理器在链路中的order值;
    }
}

定义查询用户余额是否充足的处理器

@Component
public class HavaMoneyHandler implements PurchaseFilter {

    @Override
    public void handler(PurchaseTicketReqDTO requestParam) {
        // 校验余额是否充足操作,不是就抛异常中止责任链校验操作
    }

    @Override
    public int getOrder() {
        return 处理器在链路中的order值;
    }
}

如果还有什么校验逻辑就直接加处理器就行,不需要动原代码,满足开闭原则

5.业务层代码实现

private AbstractChainContext abstractChainContext = new AbstractChainContext();

public void purchaseService(T requestParam){
    try {
        // 把参数丢进职责链中校验一番先
        abstractChainContext.handler(FilterChainMarkEnum.PURCHASE_FILTER.name(), requestParam);
    } catch (Exception e) {
        // 校验失败,打印结果
        log.error({"职责链中报错:{}"}, e.getMessage());
    }
    // 校验成功啦,正常的CRUD去吧
    xxxxxxxxx
}

        经过责任链模式的重构,你是否发现业务逻辑变得更加清晰易懂了?采用这种设计模式后,增加或删除相关的业务逻辑变得非常方便,不再需要担心更改上千行代码的几行代码,导致整个业务逻辑受到影响的情况。

文末总结 + 优化思路

本文详细介绍了责任链模式的概念,并通过商品下单场景模拟了真实使用场景。

        为了复用责任链接口定义和上下文,我们通过抽象的方式将责任链门面接口加入到基础组件库中,实现快速接入责任链的目的。

        虽然在本文中,我们没有使用 boolean 类型的返回值,而是通过异常来终止流程,但在后续的增强中,我们可以考虑添加布尔类型的返回值。

        此外,我们还可以在 AbstractChainHandler 中增加是否异步执行的方法,以提高方法执行性能和减少接口响应时间。

        架构设计总是在不断演进,本文的设计也有优化和进步的空间,让我们继续探索责任链模式的更多可能性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1144713.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Proteus仿真--左右来回流水灯仿真(仿真文件+程序)

本文主要介绍基于51单片机的流水灯仿真&#xff08;完整仿真源文件及代码见文末链接&#xff09; 仿真运行视频 Proteus仿真--基于51单片机的流水灯仿真&#xff08;左右来回&#xff09; 附完整Proteus仿真资料代码资料 百度网盘链接: https://pan.baidu.com/s/1pS1rHGOhwYgP…

springboot+vue基于协同过滤算法的私人诊所管理系统的设计与实现【内含源码+文档+部署教程】

博主介绍&#xff1a;✌全网粉丝10W,前互联网大厂软件研发、集结硕博英豪成立工作室。专注于计算机相关专业毕业设计项目实战6年之久&#xff0c;选择我们就是选择放心、选择安心毕业✌ &#x1f345;由于篇幅限制&#xff0c;想要获取完整文章或者源码&#xff0c;或者代做&am…

MySQL Server 5.5 软件和安装配置教程

MySQL 5.5.58&#xff08;32/64位&#xff09;下载链接&#xff1a; 百度网盘&#xff1a;百度网盘 请输入提取码 提取密码&#xff1a;7act 软件简介&#xff1a; MySQL 是由瑞典MySQL AB 公司开发一个关系型数据库管理系统&#xff0c;目前属于 Oracle 旗下产品。MySQL 是最…

15KW永磁同步电动机设计

摘 要 在我们日常生活中&#xff0c;永磁同步电机随处可见&#xff0c;因为其相比其他电机而言结构相对简单&#xff0c;运行稳定且便于维修等优势&#xff0c;最重要的是永磁同步电机在调速方面具有很好的优势。随着自动控制技术和微电子技术的不断革新&#xff0c;目前的技术…

她从家乡自贡起步,努力奋斗进京任职

读罢“北京文博”于2023年10月14日发表的新闻报道《文明互鉴 文明共兴——2023国际城市媒体北京论坛在京举行》&#xff0c;心中甚为欣慰。因为当年从笔者家乡走出去的70后女青年吴旭&#xff0c;现在令人惊喜地已经成长为中华全国新闻工作者协会党组成员、中宣部对外推广局局长…

高等数学前置知识——一次函数

文章目录 一次函数1.1 一次函数1.2 正比例函数1.3 一次函数的平移1.4 一次函数常用知识 一次函数 1.1 一次函数 一次函数的定义为&#xff1a;y kx b &#xff0c;k ≠ 0。 ​ 注&#xff1a;在这个函数中k代表斜率&#xff0c;b代表与y轴的截距。当 x 0 时&#xff0c;y…

挖坑法,Hoare,非递归法实现快速排序

时间&#xff1a;O(N*lgn)->最坏n^2(有序&#xff0c;逆序) 空间&#xff1a;logN N*2 Hoare Hoare法与其他快速排序算法的不同之处在于它使用两个指针&#xff08;分别指向数组的起始位置和结束位置&#xff09;&#xff0c;并通过交换元素的方式来确定基准值的最终位置。…

python 安装成功后终端显示的还是低版本

如果你下载了新版的 Python&#xff0c;但在使用时发现仍然是之前的版本&#xff0c;可能是因为新版的 Python 没有替代系统环境中的旧版 Python。 检查 PATH 环境变量&#xff1a;在命令行中输入 python --version 来查看当前默认的 Python 版本。如果显示的是旧版 Python 的…

Zotero背景设置护眼模式

一、背景知识 Zotero是开源的文献管理工具&#xff0c;功能非常强大&#xff0c;可以安装多款插件。对我来说&#xff0c;其最大的优点是可以对文章分类和打标签&#xff0c;因为某个瞬间&#xff0c;我经常想起论文中的一个图片&#xff0c;或者某个细节&#xff0c;但是却记…

Android:窗口管理器WindowManager

Android&#xff1a;窗口管理器WindowManager 导言 本篇文章主要是对Android中与窗口(Window)有关的知识的介绍&#xff0c;主要涉及到的有&#xff1a; WindowWindowManagerWindowManagerService 主要是为了更进一步地向下地深入Android屏幕渲染的知识&#xff08;虽然窗口…

vlc打开网络流(如rtmp),并查看媒体信息

打开vlc 选择媒体&#xff0c;打开网络串流 输入rtmp地址&#xff0c;点击播放 选择工具-编解码信息 可以查看节目的编码信息什么的

口袋参谋:如何快速挑选宝贝核心关键词?三种方法,简单有效!

做电商&#xff0c;一定要会选择关键词&#xff01;这是我近十年做电商的经验之谈。 不管是标题还是直通车推广&#xff0c;都需要选择与产品相关度高、符合买家搜索习惯的关键词&#xff0c;只有这样&#xff0c;才能吸引更多自然流量&#xff01;有了流量加持&#xff0c;还…

C#学习相关系列之多线程(七)---Task的相关属性用法

一、Task和Thread的区别 任务是架构在线程之上的,任务最终的执行还是要给到线程去执行的。任务和线程之间不是一对一的关系&#xff0c;任务更像线程池&#xff0c;任务相比线程池有很小的开销和精确的控制。&#xff08;总的来说Task的用法更为先进&#xff0c;在多线程的时候…

人工智能基础_机器学习008_使用正规方程_损失函数进行计算_一元一次和二元一次方程演示_sklearn线性回归演示---人工智能工作笔记0048

自然界很多都是正态分布的,身高,年龄,体重...但是财富不是. 然后我们来看一下这个y = wx+b 线性回归方程. 然后我们用上面的代码演示. 可以看到首先import numpy as np 导入numby 数据计算库 import matplotlib.pyplot as plt 然后导入图形画的库 然后: X = np.linspace(0,…

【计算机毕设经典案例】基于微信小程序的图书管理系统

前言&#xff1a;我是IT源码社&#xff0c;从事计算机开发行业数年&#xff0c;专注Java领域&#xff0c;专业提供程序设计开发、源码分享、技术指导讲解、定制和毕业设计服务 &#x1f449;IT源码社-SpringBoot优质案例推荐&#x1f448; &#x1f449;IT源码社-小程序优质案例…

数组与链表算法-数组与多项式

目录 数组与链表算法-数组与多项式 多项式数组表达式 C代码 数组与链表算法-数组与多项式 多项式是数学中相当重要的表达方式&#xff0c;如果使用计算机来处理多项式的各种相关运算&#xff0c;那么通常使用数组或链表来存储多项式。 多项式数组表达式 假如一个多项&…

网络安全(黑客技术)—小白自学

目录 一、自学网络安全学习的误区和陷阱 二、学习网络安全的一些前期准备 三、网络安全学习路线 四、学习资料的推荐 想自学网络安全&#xff08;黑客技术&#xff09;首先你得了解什么是网络安全&#xff01;什么是黑客&#xff01; 网络安全可以基于攻击和防御视角来分类&am…

PostgreSQL 基础知识

执行环境&#xff1a; psql 1. 创建一个表格 CREATE TABLE customers ( customer_id serial PRIMARY KEY,firstname VARCHAR(100) NOT NULL,lastname VARCHAR(100) NOT NULL,username VARCHAR(50) UNIQUE NOT NULL,password VARCHAR(50) NOT NULL,email VARCHAR(255) UNIQUE …

Go学习第十三章——Gin(请求与响应)

Go web框架——Gin&#xff08;请求与响应&#xff09; 1 响应1.1 String1.2 JSON&#xff08;*&#xff09;1.3 HTML&#xff08;*&#xff09;1.4 XML1.5 文件&#xff08;*&#xff09; 2 请求2.1 请求参数查询参数 (Query)动态参数 (Param)表单参数 (PostForm)原始参数 (Ge…

[导弹打飞机H5动画制作]飞机路线的随机起飞及自爆模拟

参考代码&#xff1a; this.btnOff.addEventListener("click", off.bind(this)); this.btnBomb.addEventListener("click", bomb.bind(this)); var _this this; var pb null; function off() {if (pb null) {pb new lib.sb1link();pb.x 600;pb.y 30…