当前位置: 首页 > news >正文

【设计模式】责任链的基本概念及使用Predicate灵活构造校验链

文章目录

  • 1. 概述
    • 1.1.背景
    • 1.2.责任链模式的概念
  • 2.责任链的基本写法
    • 2.1.链表实现
    • 2.2.数组实现
  • 3.Predicate校验链
    • 2.1.使用Predicate改写代码
    • 2.1.更丰富的条件拓展
  • 4.总结

1. 概述

1.1.背景

在最近的开发中遇到了这么一个需求,需要对业务流程中的各个参数做前置校验,校验通过才能执行后续的流程。
经过一番需求分析后发现,有大量的业务入口需要做的校验是相同的,同时在业务迭代的过程中涉及到的校验规则也会有所增减。所以就必须考虑到代码的复用性和拓展性,决定使用责任链模式来剥离变化。

1.2.责任链模式的概念

简单的说,就是将不同的功能封装成一个一个不同的处理器,并且将这些处理器按顺序链接成一个链表,客户端发起请求后,请求的对象会按照链的顺序被各个处理器接收并处理,最终返回处理结果。
在这里插入图片描述
这个模式最突出的特点就是每个处理器都会接收请求、处理请求,并将请求传递给下一个处理器,我们可以自由的组合处理器的顺序,也可以自由的选择需要组合哪些处理器

我们可以让每个处理器都有处理请求的机会,直到运行的链尾为止,这种方式常见于过滤器链,例如:Web开发中的FilterDubbo中的Filter,参数校验链中的or类型等等。

当然,也可以根据需要在任意一个处理器满足特定的要求后,截断处理器,不再向下传递,例如:工作流,参数校验链中的and类型等等。

2.责任链的基本写法

传统的责任链是通过链表来进行组装的,基础类图如下:
在这里插入图片描述
图中的handler上有个一个Context参数,这个是指的责任链传递过程中的上下文对象,在不同的handler中需要处理的参数字段往往是不同的,所以,我们将需要处理的参数都聚合在一起就形成了上下文context。上下文对象一般是在client调用方调用责任链处理接口时创建的。

下面通过一个简单的注册账号的需求,来体验一下责任链模式,即:校验用户名/密码不能为空,且年龄大于18岁才能注册。

2.1.链表实现

需要再每个handler中保存下一个handler的指针,通过聚合来处理

  • 下面是抽象处理器和两个处理器实例:
    public abstract class AbstractHandler {/*** 下一个处理器*/private AbstractHandler nextHandler;/*** 设置下一个处理器*/public AbstractHandler setNextHandler(AbstractHandler nextHandler) {this.nextHandler = nextHandler;return this;}/*** 处理器调用及传递到下一个处理器*/public boolean handle(Context context) {boolean result = doHandle(context);if (result && nextHandler != null) {return nextHandler.handle(context);}return result;}/*** 实际的处理规则,由子类实现*/public abstract boolean doHandle(Context context);}
    import org.apache.commons.lang3.StringUtils;public class UserNameHandler extends AbstractHandler {@Overridepublic boolean doHandle(Context context) {if (StringUtils.isBlank(context.getUsername()) || StringUtils.isBlank(context.getPassword())) {System.out.println("用户名或密码不能为空");return false;}return true;}
    }
    
    public class AgeHandler extends AbstractHandler {@Overridepublic boolean doHandle(Context context) {if (context.getAge() < 18) {System.out.println("18岁以下不能注册");return false;}return true;}
    }
  • 提供一个上下文聚合用户名、密码、年龄
public class Context {private String username;private String password;private int age;// 省略getter setter
}
  • 在client中组装责任链
    public class Client {public String register(String username, String password, int age) {AbstractHandler handler = new UserNameHandler().setNextHandler(new AgeHandler());Context context = new Context();context.setUsername(username);context.setPassword(password);context.setAge(age);boolean handle = handler.handle(context);return handle ? "注册成功" : "注册失败";}
    }
    

写完后测试一下:

public static void main(String[] args) {Client client = new Client();System.out.println(client.register("张三", "123456", 20));System.out.println("------------");System.out.println(client.register("张三", "123456", 17));System.out.println("------------");System.out.println(client.register("", "123456", 20));System.out.println("------------");System.out.println(client.register("张三", "", 20));
}

在这里插入图片描述

2.2.数组实现

数组的实现实际上就是将上面链表中的next指针去掉,通过数组下标来决定责任链执行的顺序,此时的抽象类可以简化为:

public abstract class AbstractHandler {/*** 实际的处理规则,由子类实现*/public abstract boolean doHandle(Context context);}

处理器实例和上下文类不用做修改,在客户端组装并调用责任链:

public class Client {public String register(String username, String password, int age) {List<AbstractHandler> list = new ArrayList<>();list.add(new UserNameHandler());list.add(new AgeHandler());Context context = new Context();context.setUsername(username);context.setPassword(password);context.setAge(age);boolean result = false;for (AbstractHandler handler : list) {result = handler.doHandle(context);if (!result) {break;}}return result ? "注册成功" : "注册失败";}
}

使用同样的测试用例测试,得到同样的结果:在这里插入图片描述
相信大家注意到了,这种方式实现责任链,客户端与责任链并没有解耦,在客户端中使用for循环中操作了链的开始和截断,严格的说这种方式属于是责任链的变体。

这么做的好处就是把各个处理器只是当做了单读的校验处理器个体,只起到复用的作用,我们在不同的客户端中可以自由的组合自己想要的校验链,使用更加灵活。


现在再思考一个问题:如果想把and校验改成or校验应该怎么做呢?

我们修改for循环中对结果的处理方法,只有在校验结果为true时赋值就可以了。但这种方式既不灵活也不优雅,假如我们在条件中既要有and又要有or时,应该怎么处理呢?使用这种方式处理就变得很复杂了。
如果使用的JDK版本是8以上的话,可以使用JDK提供的函数式接口Predicate接口灵活构造校验链。

3.Predicate校验链

在这里插入图片描述
在这个接口中一共有5个方法,其中:1个抽象发放,3个默认方法,以及一个静态方法,我们这里需要使用到的是test,and,or
对比上面的责任链模式来看的话,可以把Predicate简单的理解为抽象处理器,test是抽象方法doHandle由子类实现具体的逻辑,T则可以理解为传入的上下文Contextandor则是用来组装调用链的,区别在于这里的andor是将函数拼接了起来,而不是保存下一个处理器的指针。

2.1.使用Predicate改写代码

不再需要AbstractHandler,两个处理器实例修改为实现Predicate,代码如下:

import java.util.function.Predicate;public class AgeHandler implements Predicate<Context> {@Overridepublic boolean test(Context context) {if (context.getAge() < 18) {System.out.println("18岁以下不能注册");return false;}return true;}
}
import org.apache.commons.lang3.StringUtils;import java.util.function.Predicate;public class UserNameHandler implements Predicate<Context> {@Overridepublic boolean test(Context context) {if (StringUtils.isBlank(context.getUsername())|| StringUtils.isBlank(context.getPassword())) {System.out.println("用户名或密码不能为空");return false;}return true;}
}

在客户端中通过and方法来组合校验链:

public class Client {public String register(String username, String password, int age) {Context context = new Context();context.setUsername(username);context.setPassword(password);context.setAge(age);Predicate<Context> predicate = new UserNameHandler().and(new AgeHandler());boolean test = predicate.test(context);return test ? "注册成功" : "注册失败";}

测试后依然是一样的结果:
在这里插入图片描述


这里简单的说明一下and方法,

default Predicate<T> and(Predicate<? super T> other) {Objects.requireNonNull(other);return (t) -> test(t) && other.test(t);
}

根据上面的拼接方式,是由userNameHandler调用的and方法,传入了参数ageHandler,所以这里左侧的test(t)指的调用是UserNameHandler中重写的test方法,右侧的other.test(t),指的是AgeHandler重写的test方法,两个方法返回的boolean通过&&连接起来。这里我加上变量名的话,可能会更好理解一点:

return (t) -> this.test(t) && new AgeHandler().test(t);

需要注意的是,&&两侧的test并不是在and方法中对两个handler对象发起了调用,可以看到return后面还有一个(t)->,这个表示的是返回的是一个函数,这个函数会在调用predicate 这个接口对象的test方法(函数式接口的唯一抽象方法,这里这个抽象方法名为test)时触发,除非后才会执行test(t) && other.test(t)

如果我们还有更多的handler继续往下拼接的话,例如有a,b,c,d4个实现了Predicate的对象,并且都用and拼接,整个函数可以理解为转化成了如下的链:

return (t) -> a.test(t) && b.test(t) && c.test(t) && d.test(t);

2.1.更丰富的条件拓展

现在来实现一个更丰满的需求,在上面需求的基础上加入一个新的校验规则,在注册时需要加入用户的联系方式,手机号和邮箱选其一即可。

我们可以在context中追加这两个参数,并新增两个处理器,通过or进行拼接。

public class Context {private String username;private String password;private String phone;private String email;private int age;// 忽略 getter setter
}
import org.apache.commons.lang3.StringUtils;
import java.util.function.Predicate;public class PhoneHandler implements Predicate<Context> {@Overridepublic boolean test(Context context) {if (StringUtils.isBlank(context.getPhone())) {System.out.println("手机号为空");return false;}System.out.println("手机号不为空");return true;}
}
import org.apache.commons.lang3.StringUtils;
import java.util.function.Predicate;public class EmailHandler implements Predicate<Context> {@Overridepublic boolean test(Context context) {if (StringUtils.isBlank(context.getEmail())) {System.out.println("邮箱为空");return false;}System.out.println("邮箱不为空");return true;}
}

client中,我们只需要实现将校验链函数拼接成如下的形式:

return (t) -> this.test(t) && new AgeHandler().test(t) && (new PhoneHandler().test(t) || new EmailHandler().test(t));

则实现代码为:

Predicate<Context> predicate = new UserNameHandler().and(new AgeHandler()).and(new PhoneHandler().or(new EmailHandler()));

写两个测试用例测一下:

public static void main(String[] args) {Client client = new Client();System.out.println(client.register("张三", "123456", 18, "12345678901", ""));System.out.println("------------");System.out.println(client.register("张三", "123456", 18, "", "xxx@qq.com"));System.out.println("------------");
}

在这里插入图片描述

4.总结

本篇先简单的讲解了责任链模式的概念,通过一个简单的注册用户需求代入了责任链的实现方式,并提出了用责任链是实现校验时灵活的地方。

然后针对这种不灵活,通过Java8中的Predicate对校验链进行改写,更加灵活的实现了校验链的构造,并且拓展起来也是非常简单的。

希望这种使用方式对大家能有所启发和帮助。

相关文章:

【设计模式】责任链的基本概念及使用Predicate灵活构造校验链

文章目录 1. 概述1.1.背景1.2.责任链模式的概念 2.责任链的基本写法2.1.链表实现2.2.数组实现 3.Predicate校验链2.1.使用Predicate改写代码2.1.更丰富的条件拓展 4.总结 1. 概述 1.1.背景 在最近的开发中遇到了这么一个需求&#xff0c;需要对业务流程中的各个参数做前置校验…...

Taro保存图片到手机

萌新亚历山大啊&#xff0c;搞了一下午&#xff0c;真多坑 Taro.downloadFile({url: res,filePath: Taro.env.USER_DATA_PATH /xcxcode.jpg,success: res > {if (res.statusCode 200) {console.log(res)const tempFilePath res.filePath; // 获取下载的临时文件路径// …...

zookeeper --- 基础篇

一、zookeeper简介 1.1、什么是zookeeper zookeeper官网&#xff1a;https://zookeeper.apache.org/ 大数据生态系统里的很多组件的命名都是某种动物或者昆虫&#xff0c;他是用来管 Hadoop&#xff08;大象&#xff09;、Hive(蜜蜂)、Pig(小 猪)的管理员。顾名思义就是管理…...

Golang map 常用方法

文章目录 前言按key排序按value排序统计字符串中元素重复出现次数并将结果排序返回 前言 由于map是无序的&#xff0c;所以排序成 slice 返回&#xff0c;且都使用泛,代码只是示例&#xff0c;省略了错误判断 按key排序 package mainimport "fmt"func example[T c…...

用于视觉跟踪的在线特征选择研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

Docker基本使用

查看本地镜像 查看本地&#xff1a;docker imagesPull镜像&#xff1a;docker pull nginx:latest登录镜像&#xff1a;docker login hub.docker.com -u **** -p ****制作镜像&#xff1a;docker build -t xxxx:v1push&#xff1a;docker push xxx:v1删除镜像:docker rmi #imag…...

PHP生成随机数

要在 PHP 中生成随机密码&#xff0c;可以使用 rand() 函数和字符串操作函数。以下是一个示例代码来生成包含字母、数字和特殊字符的随机密码 function generateRandomPassword($length 8) {// 定义包含字母、数字和特殊字符的字符集$characters abcdefghijklmnopqrstuvwxyz…...

MongoDB:Unrecognized option: storage

MongoDB一直显示 Unrecognized option: storage try ‘mongod --help’ for more information 意思是我们配置的config文件出了问题。 说明&#xff1a;MongoDB采用的是YAML格式&#xff0c;所以我们只需要稍微改改就好。 在storage前面&#xff1a;没有空格 下面两行最前面…...

Java基础九 - 异常

1. throw 和 throws 1. throw是在代码运行过程中引起一个异常 2. throws是将异常声明但是不处理&#xff0c;而是将异常往上传&#xff0c;谁调用我就交给谁处理 2. final、finally、finalize 1. final是修饰符&#xff0c;用于修饰变量、方法、类修饰变量&#xff0c;最开始…...

[国产MCU]-BL602开发实例-UART数据发送与接收

UART数据发送与接收 文章目录 UART数据发送与接收1、BL602的UART介绍2、UART相关驱动API介绍3、UART使用示例3.1 轮询方式数据接收与发送3.2 中断方式数据接收与发送3.3 DMA方式接收与发送数据通用异步收发传输器(Universal Asynchronous Receiver/Transmitter,通常称为UART)…...

mac安装open3d时候出现错误

在测试open3d是否正常顺利安装时&#xff0c;出现了如下错误&#xff1a; python -c "import open3d as o3d; print(o3d.__version__)" Traceback (most recent call last):File "<string>", line 1, in <module>File "/Users/huangzhe/…...

打造本地户外装备小程序商城教程大揭秘

在如今的移动互联网时代&#xff0c;小程序已经成为了各行各业的发展利器。尤其对于户外用具行业来说&#xff0c;一个专属的小程序商城将能够极大地提升企业的品牌形象和销售业绩。下面就来介绍一下快速上手制作户外用具小程序的攻略吧。 首先&#xff0c;登录乔拓云平台进入商…...

在外SSH远程连接Ubuntu系统

在外SSH远程连接Ubuntu系统【无公网IP】 文章目录 在外SSH远程连接Ubuntu系统【无公网IP】前言1. 在Ubuntu系统下安装cpolar软件2. 完成安装后打开cpolar客户端web—UI界面3. 创建隧道取得连接Ubuntu系统公网地址4. 打开Windows的命令界面并输入命令 前言 随着科技和经济的发展…...

GOTS标准速递---7.0 版将于2024年3月1日起全面生效

【GOTS标准速递---7.0 版将于2024年3月1日起全面生效】 全球有机纺织品标准很高兴地宣布发布GOTS 7.0 版&#xff0c;该版本扩大了环境和社会标准的范围&#xff0c;同时保持了适用于工业生产并适用于各种产品的标准。在为期一年的定期修订过程中&#xff0c;在有机生产、纺织品…...

Ansible的安装和配置

安装和配置 Ansible 安装所需的软件包 创建名为 /home/greg/ansible/inventory 的静态清单文件&#xff0c;以满足以下要求&#xff1a; 172.25.250.9 是 dev 主机组的成员 172.25.250.10 是 test 主机组的成员 172.25.250.11 和 172.25.250.12 是 prod 主机组的成员 172.2…...

口-肠-脑轴与精神健康的关系

谷禾健康 在个体中&#xff0c;每个微生物栖息地都表现出独特的微生物种群模式。迄今为止&#xff0c;关于微生物组相关疾病的研究主要集中在器官特异性微生物组上。然而&#xff0c;器官间的微生物网络正逐渐成为生理功能和病理过程中的重要调节因子和治疗机会。 在正常情况下…...

Dubbo是干嘛的,Dubbo原理和机制,Dubbo的核心组件

目录 一、介绍1、Dubbo是什么2、为什么需要Dubbo3、Dubbo的特性 二、 Dubbo的核心概念1、暴露和引用&#xff08;Export and Refer&#xff09;2、服务提供者和服务消费者3、注册中心4、负载均衡5、集群容错 三、Dubbo的架构1、服务提供者和服务消费者之间的通信流程2、Dubbo的…...

自动zksync刷账户交互(附代码)

自动化任务的 Python 代码。它使用 Selenium 库来控制浏览器。 #解锁小狐狸 task.unlock_metamask(ads) #zk主网连接钱包,初始化 #ZK主网任务1:转账 print(选择ZK主网任务1:转账) task.zk_task1(ads,address) #…...

荐读 | 《揭秘云计算与大数据》

当我们回顾过去几十年的科技进步时&#xff0c;云计算和大数据在现代科技发展史上无疑具有里程碑式的意义&#xff0c;它们不仅改变了我们的生活方式&#xff0c;而且对各行各业产生了深远的影响。 在这个数字化时代&#xff0c;云计算和大数据技术已经成为推动全球发展的关键…...

RISC-V基础之内存分布与映射

内存映射是指将地址空间划分为不同的部分或段&#xff0c;每个段有不同的用途和属性。这段话介绍了五个段&#xff1a;文本段、全局数据段、动态数据段、异常处理器段和操作系统&#xff08;OS&#xff09;段。下面是对每个段的简要说明&#xff1a; 文本段&#xff1a;存放程序…...

Flask RESTful 示例

目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题&#xff1a; 下面创建一个简单的Flask RESTful API示例。首先&#xff0c;我们需要创建环境&#xff0c;安装必要的依赖&#xff0c;然后…...

python/java环境配置

环境变量放一起 python&#xff1a; 1.首先下载Python Python下载地址&#xff1a;Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个&#xff0c;然后自定义&#xff0c;全选 可以把前4个选上 3.环境配置 1&#xff09;搜高级系统设置 2…...

MODBUS TCP转CANopen 技术赋能高效协同作业

在现代工业自动化领域&#xff0c;MODBUS TCP和CANopen两种通讯协议因其稳定性和高效性被广泛应用于各种设备和系统中。而随着科技的不断进步&#xff0c;这两种通讯协议也正在被逐步融合&#xff0c;形成了一种新型的通讯方式——开疆智能MODBUS TCP转CANopen网关KJ-TCPC-CANP…...

反射获取方法和属性

Java反射获取方法 在Java中&#xff0c;反射&#xff08;Reflection&#xff09;是一种强大的机制&#xff0c;允许程序在运行时访问和操作类的内部属性和方法。通过反射&#xff0c;可以动态地创建对象、调用方法、改变属性值&#xff0c;这在很多Java框架中如Spring和Hiberna…...

vue3 定时器-定义全局方法 vue+ts

1.创建ts文件 路径&#xff1a;src/utils/timer.ts 完整代码&#xff1a; import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...

c#开发AI模型对话

AI模型 前面已经介绍了一般AI模型本地部署&#xff0c;直接调用现成的模型数据。这里主要讲述讲接口集成到我们自己的程序中使用方式。 微软提供了ML.NET来开发和使用AI模型&#xff0c;但是目前国内可能使用不多&#xff0c;至少实践例子很少看见。开发训练模型就不介绍了&am…...

使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度

文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...

Linux离线(zip方式)安装docker

目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1&#xff1a;修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本&#xff1a;CentOS 7 64位 内核版本&#xff1a;3.10.0 相关命令&#xff1a; uname -rcat /etc/os-rele…...

保姆级教程:在无网络无显卡的Windows电脑的vscode本地部署deepseek

文章目录 1 前言2 部署流程2.1 准备工作2.2 Ollama2.2.1 使用有网络的电脑下载Ollama2.2.2 安装Ollama&#xff08;有网络的电脑&#xff09;2.2.3 安装Ollama&#xff08;无网络的电脑&#xff09;2.2.4 安装验证2.2.5 修改大模型安装位置2.2.6 下载Deepseek模型 2.3 将deepse…...

RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)

RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发&#xff0c;后来由Pivotal Software Inc.&#xff08;现为VMware子公司&#xff09;接管。RabbitMQ 是一个开源的消息代理和队列服务器&#xff0c;用 Erlang 语言编写。广泛应用于各种分布…...