配套代码: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 设计模式学习指南

上一篇:代理 · 下一篇:命令