文章目录
- 1. 概述
- 1.1.背景
- 1.2.责任链模式的概念
- 2.责任链的基本写法
- 2.1.链表实现
- 2.2.数组实现
- 3.Predicate校验链
- 2.1.使用Predicate改写代码
- 2.1.更丰富的条件拓展
- 4.总结
1. 概述
1.1.背景
在最近的开发中遇到了这么一个需求,需要对业务流程中的各个参数做前置校验,校验通过才能执行后续的流程。
经过一番需求分析后发现,有大量的业务入口需要做的校验是相同的,同时在业务迭代的过程中涉及到的校验规则也会有所增减。所以就必须考虑到代码的复用性和拓展性,决定使用责任链模式来剥离变化。
1.2.责任链模式的概念
简单的说,就是将不同的功能封装成一个一个不同的处理器,并且将这些处理器按顺序链接成一个链表,客户端发起请求后,请求的对象会按照链的顺序被各个处理器接收并处理,最终返回处理结果。
这个模式最突出的特点就是每个处理器都会接收请求、处理请求,并将请求传递给下一个处理器,我们可以自由的组合处理器的顺序,也可以自由的选择需要组合哪些处理器。
我们可以让每个处理器都有处理请求的机会,直到运行的链尾为止,这种方式常见于过滤器链,例如:Web开发中的Filter
,Dubbo
中的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 { @Override public 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 { @Override public 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
则可以理解为传入的上下文Context
。and
和or
则是用来组装调用链的,区别在于这里的and
和or
是将函数拼接了起来,而不是保存下一个处理器的指针。
2.1.使用Predicate改写代码
不再需要AbstractHandler
,两个处理器实例修改为实现Predicate
,代码如下:
import java.util.function.Predicate;
public class AgeHandler implements Predicate<Context> {
@Override
public 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> {
@Override
public 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,d
4个实现了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> {
@Override
public 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> {
@Override
public 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
对校验链进行改写,更加灵活的实现了校验链的构造,并且拓展起来也是非常简单的。
希望这种使用方式对大家能有所启发和帮助。