15|责任链:让请求按明确规则经过处理节点
配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为
com.hanserwei.patterns.chain。
系列导航:Java 25 设计模式学习指南
文章发布需要检查标题和权限。最初把它们写进一个方法很简单,但不同入口逐渐需要不同校验组合,且失败提示取决于先检查哪一项。继续复制 if 会让规则顺序分散。
责任链把每种规则封装成节点,在装配时决定顺序。本章选用“校验失败即停止,成功继续”的链;它不是唯一形式,经典请求链也常见“当前节点能处理就终止,否则转交下一节点”。
从问题提炼设计意图
责任链使请求沿一组处理者传递,发送方无需耦合每个具体处理者;是否继续、终止条件及链尾行为必须明确。
对象职责与协作关系
| 示例角色 | 职责 |
|---|---|
Submission |
待检查请求,包含标题和发布权限 |
SubmissionCheck |
抽象处理者,统一管理继续与短路 |
TitleCheck、PermissionCheck |
具体规则节点 |
Demo |
装配链并从链头发起校验 |
classDiagram
SubmissionCheck <|-- TitleCheck
SubmissionCheck <|-- PermissionCheck
SubmissionCheck --> SubmissionCheck : next
SubmissionCheck ..> Submission : validates
代码思路:变化应该落在哪个对象上
next 在构造时固定,让一条链的结构在使用期间不再被随意更改。链尾以 null 表示;这只发生在内部结构中,业务结果用 Optional
final check 先执行本节点的 validate。发现错误时立即返回;没有后继时返回当前结果;只有当前成功且后继存在时,才递归调用 next.check。具体节点不重复写链条控制代码,减少某个节点忘记继续执行的风险。
标题与权限同时失败时,调换节点顺序会改变报告的首个错误。因此顺序是业务规则的一部分,而不只是容器里怎样排列对象。权限敏感系统还要考虑是否应该先拒绝未授权访问,以免泄漏后续校验信息。
关键实现与独立运行
配套仓库中的包名是 com.hanserwei.patterns.chain,源码目录为 src/main/java/com/hanserwei/patterns/chain/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。
SubmissionCheck.java:
package com.hanserwei.patterns.chain;
import java.util.Objects;
import java.util.Optional;
/** 失败即终止、成功向后传递的责任链节点. */
public abstract class SubmissionCheck {
/** 后继节点;null 表示链尾. */
private final SubmissionCheck next;
/** 在构造时固定链结构. */
protected SubmissionCheck(SubmissionCheck next) {
this.next = next;
}
/** 返回首个错误;空 Optional 表示整条链通过. */
public final Optional<String> check(Submission submission) {
Objects.requireNonNull(submission, "submission");
Optional<String> failure = validate(submission);
if (failure.isPresent() || next == null) {
return failure;
}
return next.check(submission);
}
/** 执行当前节点的校验,不负责后继调用. */
protected abstract Optional<String> validate(Submission submission);
}
TitleCheck.java:
package com.hanserwei.patterns.chain;
import java.util.Optional;
/** 负责一种独立发布规则的处理节点. */
public final class TitleCheck extends SubmissionCheck {
/** 装配后继节点;链尾传入 null. */
public TitleCheck(SubmissionCheck next) {
super(next);
}
/** 返回本规则的失败原因,成功时允许后续规则继续检查. */
@Override
protected Optional<String> validate(Submission submission) {
if (submission.title() == null || submission.title().isBlank()) {
return Optional.of("blank title");
}
return Optional.empty();
}
}
PermissionCheck.java:
package com.hanserwei.patterns.chain;
import java.util.Optional;
/** 负责一种独立发布规则的处理节点. */
public final class PermissionCheck extends SubmissionCheck {
/** 装配后继节点;链尾传入 null. */
public PermissionCheck(SubmissionCheck next) {
super(next);
}
/** 返回本规则的失败原因,成功时允许后续规则继续检查. */
@Override
protected Optional<String> validate(Submission submission) {
if (!submission.permitted()) {
return Optional.of("permission denied");
}
return Optional.empty();
}
}
Demo.java 展示调用方如何装配这些对象:
package com.hanserwei.patterns.chain;
/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
/** 禁止实例化演示入口. */
private Demo() {}
/** 运行独立示例;args 为未使用的命令行参数. */
public static void main(String[] args) {
SubmissionCheck chain = new TitleCheck(new PermissionCheck(null));
System.out.println(chain.check(new Submission("Java 25", true)).orElse("approved"));
}
}
在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew:
./gradlew runChain
./gradlew test --tests 'com.hanserwei.patterns.chain.PatternTest'
示例的业务输出如下,省略 Gradle 自身的任务提示:
approved
用测试确认模式的行为
同时失败时返回首节点错误;标题通过但权限失败时确实进入后继;全部规则通过时得到空 Optional。
对应测试位于 src/test/java/com/hanserwei/patterns/chain/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。
常见用法
- 表单、发布和请求进入业务前的可组合校验。
- 日志处理、事件处理或请求路由逐个寻找合适的处理者。
- 需要按配置组合拦截步骤,同时保持节点职责独立。
适用边界与容易踩的坑
本例只返回第一个错误,不收集全部错误。如果产品要求同时展示所有字段错误,应改变结果模型和遍历策略,而不是在每个节点里偷偷调用后继再拼字符串。
要明确“无人处理”的含义。本例链尾成功代表全部校验通过;对于请求路由链,无人匹配可能必须抛出异常或返回未处理标志。照搬同一链尾逻辑会造成错误批准。
构造不可变链可以降低成环风险,但外部扩展节点仍要遵守 validate 不递归乱调用的约定。非常长的递归链也可能需要迭代实现。不要在链中混入未经定义的部分写入,否则前面成功、后面失败后的状态很难恢复。
与相近模式比较
装饰器通常让请求向内委托后再向外增强结果;责任链可以在中途拒绝或终止处理。管道往往让每一步都转换数据,责任链则更强调是否传递处理权。
动手练习
新增长度校验,把它分别放到权限检查前后,写出三种同时失败输入的预期结果。再设计“收集全部错误”版本,比较它与当前短路链在结果类型和控制流上的差别。
系列导航:Java 25 设计模式学习指南