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

news/2025/2/12 17:59:16/

文章目录

  • 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对校验链进行改写,更加灵活的实现了校验链的构造,并且拓展起来也是非常简单的。

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


http://www.ppmy.cn/news/1015031.html

相关文章

TCP三次握手四次断开

一、了解TCP &#x1f345;TCP &#xff1a;传输控制协议&#xff0c;是一种面向连接的可靠的传输协议。 什么是可靠的传输协议&#xff1f;如何保障可靠传输&#xff1f; 保证可靠性&#xff1a; 1.确认机制 2.重传输机制什么是面向连接&#xff1f;如何保障面…

Redis实战案例27-UV统计

1. Redis的HyperLogLog的统计功能 示例&#xff1a; 表明HyperLogLog不管加入重复元素多少次都不会让count&#xff0c;不会计数重复元素&#xff0c;所以适合做UV计数 2. 简单实现UV测试 通过单元测试&#xff0c;向 HyperLogLog 中添加 100 万条数据&#xff0c;看看内存占…

第十五次CCF计算机软件能力认证

第一题&#xff1a;小明上学 小明是汉东省政法大学附属中学的一名学生&#xff0c;他每天都要骑自行车往返于家和学校。 为了能尽可能充足地睡眠&#xff0c;他希望能够预计自己上学所需要的时间。 他上学需要经过数段道路&#xff0c;相邻两段道路之间设有至多一盏红绿灯。 京…

看完秒懂:5G技术会给视频监控行业带来怎样的变革?

5G是第五代移动通信技术&#xff0c;能够提供更高的带宽和更快的传输速度&#xff0c;这将为视频技术的发展带来大量机会。随着5G技术的逐步普及与商用&#xff0c;人们将能够享受到更加流畅的高清视频体验&#xff0c;并且5G技术还拥有更低的延迟和更高的网络容量。这些优势不…

5.PyCharm基础使用及快捷键

在前几篇文章中介绍了PyCharm的安装和汉化,本篇文章一起来看一下PyCharm的基本用法和一些快捷键的使用方法。 本篇文章PyCharm的版本为PyCharm2023.2 新建项目和运行 打开工具,在菜单中——文件——新建项目 选择项目的创建位置(注意最好不要使用中文路径和中文名项目名称…

代码随想录算法训练营第42天| 01背包问题,你该了解这些! 滚动数组 416. 分割等和子集

今日学习的文章链接&#xff0c;或者视频链接 第九章 动态规划part04 自己看到题目的第一想法 看完代码随想录之后的想法 416 class Solution { public:bool canPartition(vector<int>& nums) {int sum 0;vector<int> dp(10001, 0);for (int i 0; i &l…

数组(六)-- LC[1851] 包含每个查询的最小区间

1 包含每个查询的最小区间 1.1 题目描述 给你一个二维整数数组 intervals &#xff0c;其中 i n t e r v a l s [ i ] [ l e f t i , r i g h t i ] intervals[i] [left_i, right_i] intervals[i][lefti​,righti​] 表示第 i i i 个区间开始于 l e f t i left_i lefti​…

navigator对象

navigator 对象是 JavaScript 中的一个内置对象&#xff0c;表示当前浏览器的信息和状态。 它提供了访问浏览器相关信息的属性和方法。下面是一些 navigator 对象的常见属性和方法&#xff1a; navigator.userAgent&#xff1a;返回包含浏览器用户代理字符串的字符串。可以使用…