行为型模式(中)——责任链、命令与迭代器
摘要
行为型模式的中间部分涵盖三个各有深度的经典模式。责任链模式(Chain of Responsibility) 将请求的处理者组织成一条链,请求沿链传递,直到被某个处理者处理或抵达链尾——这是 Netty ChannelPipeline、Servlet Filter Chain、Spring Security 过滤器链的核心设计;深度剖析”纯责任链”与”变体责任链”的区别,以及链的动态构建与执行控制机制。命令模式(Command) 将”请求”封装为对象,使得请求可以被参数化、队列化、记录日志,并支持撤销/重做——这是操作历史、宏录制、事务脚本的理论基础;分析命令模式如何将”操作”从业务逻辑中解耦,以及 Java 中函数式接口对命令模式的简化。迭代器模式(Iterator) 提供一种方法来顺序访问聚合对象中的各个元素,而无需暴露该对象的内部表示——这看起来最简单,却是 Java 集合框架设计的基石;深度分析为什么”将遍历逻辑分离出来”比看起来更有价值,以及内部迭代器(Stream API)与外部迭代器的本质区别。
第 1 章 责任链模式(Chain of Responsibility)
1.1 动机:多个处理者的顺序处理问题
责任链模式(Chain of Responsibility Pattern)解决的问题是:有多个对象都有可能处理同一个请求,但在编写代码时不知道应该由哪个对象处理,或者需要多个对象按顺序处理同一个请求。
来看两类不同的场景,它们都适合责任链,但侧重点不同:
场景一:审批流程(“哪个处理者处理”型)
请假审批:请假时间 ≤ 1 天由组长审批;1 天 < 请假时间 ≤ 3 天由经理审批;请假时间 > 3 天由 HR 总监审批。当一个请假请求来临时,系统应该自动路由到合适的审批层级,调用方不应该知道”谁来审批”——这个决定应该由审批链自己来做。
场景二:HTTP 过滤器链(“所有处理者依次处理”型)
一个 HTTP 请求进入 Web 应用时,通常要经历:认证检查 → 日志记录 → 限流检查 → 参数校验 → 业务处理。每个处理步骤都需要执行,且顺序固定。如果不用责任链,这些逻辑只能全部写在一个地方,或者通过反复传参来传递请求对象。
没有责任链时,上述逻辑通常会演变成这样的嵌套地狱:
// 反例:所有处理逻辑写死在一起,无法动态增减
public Response processRequest(Request req) {
// 认证
if (!authService.isAuthenticated(req)) {
return Response.unauthorized();
}
// 限流
if (!rateLimiter.tryAcquire(req.getIp())) {
return Response.tooManyRequests();
}
// 参数校验
ValidationResult validation = validator.validate(req);
if (!validation.isValid()) {
return Response.badRequest(validation.getErrors());
}
// 日志
logger.log(req);
// 实际业务
return businessHandler.handle(req);
}这段代码把所有处理逻辑固化在一处——新增一个处理步骤(如”IP 黑名单检查”)必须修改这个方法;移除一个步骤也要修改这里;不同 API 端点可能需要不同的处理链组合,而这里的链是硬编码的。
1.2 责任链模式的结构
责任链模式的核心是:每个处理者持有下一个处理者的引用,处理者决定是自己处理请求,还是将请求传递给下一个处理者(或两者都做)。
// 处理者接口:定义处理方法和设置下一个处理者的方法
public abstract class RequestHandler {
private RequestHandler next; // 链中的下一个处理者
// 链式设置方法(支持 handler1.setNext(handler2).setNext(handler3) 的调用)
public RequestHandler setNext(RequestHandler next) {
this.next = next;
return next; // 返回 next,支持链式调用
}
// 处理请求的抽象方法(模板方法模式)
public abstract Response handle(Request request);
// 将请求传递给下一个处理者(子类调用此方法传递)
protected Response passToNext(Request request) {
if (next != null) {
return next.handle(request);
}
// 链的末尾:无人处理,返回默认响应(或抛出异常)
return Response.notHandled();
}
}
// 具体处理者一:认证检查
public class AuthHandler extends RequestHandler {
private final AuthService authService;
@Override
public Response handle(Request request) {
if (!authService.isAuthenticated(request.getToken())) {
// 认证失败:直接返回,不传递给下一个处理者
return Response.unauthorized("Invalid or expired token");
}
// 认证成功:传递给下一个处理者继续处理
return passToNext(request);
}
}
// 具体处理者二:限流检查
public class RateLimitHandler extends RequestHandler {
private final RateLimiter rateLimiter;
@Override
public Response handle(Request request) {
if (!rateLimiter.tryAcquire(request.getClientIp())) {
return Response.tooManyRequests("Rate limit exceeded");
}
return passToNext(request);
}
}
// 具体处理者三:参数校验
public class ValidationHandler extends RequestHandler {
private final Validator validator;
@Override
public Response handle(Request request) {
ValidationResult result = validator.validate(request.getBody());
if (!result.isValid()) {
return Response.badRequest(result.getErrors());
}
return passToNext(request);
}
}
// 具体处理者四:业务处理(链的终点)
public class BusinessHandler extends RequestHandler {
private final BusinessService service;
@Override
public Response handle(Request request) {
// 业务处理不需要传递给下一个处理者(它是终点)
return service.process(request);
}
}
// 组装责任链:灵活配置不同的链结构
RequestHandler chain = new AuthHandler(authService);
chain.setNext(new RateLimitHandler(rateLimiter))
.setNext(new ValidationHandler(validator))
.setNext(new BusinessHandler(businessService));
// 执行:从链头开始处理
Response response = chain.handle(incomingRequest);1.3 纯责任链 vs 变体责任链
GoF 定义的”纯”责任链(Pure Chain)有一个严格约束:每个请求只被链中的一个处理者处理——处理者要么处理请求,要么传递给下一个,不能同时做两件事。
但在工程实践中,更常见的是”变体”责任链:每个处理者都会处理请求(做一些事情),然后可以选择是否继续传递。Servlet Filter、Netty ChannelPipeline 都是这种变体:
// 变体责任链:每个处理者都执行,然后可以选择是否继续
public class LoggingFilter extends RequestHandler {
@Override
public Response handle(Request request) {
log.info("[{}] → {} {}", request.getRequestId(), request.getMethod(), request.getPath());
long start = System.currentTimeMillis();
Response response = passToNext(request); // 先传递,等后续处理完
// 后续处理完成后,再记录响应日志("前置+后置"处理)
log.info("[{}] ← {} {}ms", request.getRequestId(),
response.getStatus(), System.currentTimeMillis() - start);
return response;
}
}这种”包裹式”处理(前置逻辑 → passToNext() → 后置逻辑)是 Servlet Filter 和 Netty ChannelPipeline 的标准写法,它让每个处理者可以在请求到达时和响应返回时各执行一段逻辑。
1.4 Netty ChannelPipeline:责任链模式的工业级实现
Netty 的 ChannelPipeline 是责任链模式在网络编程领域最完善的实现。它有以下特点:
- 双向链表:不同于单向的责任链,
ChannelPipeline维护一个双向链表,入站事件(数据到达)从 Head 向 Tail 传播,出站事件(写数据)从 Tail 向 Head 传播; - 动态修改:可以在运行时向 Pipeline 中添加或删除 Handler(
addLast()、remove()),实现动态协议切换(如 TLS 握手完成后替换 Handler); - 事件传播控制:
ChannelHandler通过调用ctx.fireChannelRead()决定是否将事件传递给下一个处理者,不调用则中断传播; - ExecutorGroup 支持:可以为不同的 Handler 指定不同的执行线程池,实现 I/O 线程与业务线程的隔离。
// Netty 责任链的配置示例(来自 Netty 系列文章的知识)
ch.pipeline()
.addLast("ssl", new SslHandler(engine)) // TLS 解密/加密
.addLast("codec", new HttpServerCodec()) // HTTP 编解码
.addLast("aggregator", new HttpObjectAggregator(65536)) // 聚合 HTTP 消息
.addLast("handler", new BusinessHandler()); // 业务处理这个 Pipeline 的每个 Handler 职责单一、可独立测试、可自由组合,正是责任链模式的精髓。
第 2 章 命令模式(Command)
2.1 动机:将”操作”对象化
命令模式(Command Pattern)的思想是:将一个操作(方法调用)封装为一个独立的对象。这看起来很奇怪——为什么要把一个方法调用变成一个对象?原因在于,当操作被封装为对象后,它就具备了所有对象的能力:可以被传递、存储、序列化、排队、记录日志,最重要的是可以被撤销(Undo)。
没有命令模式时,“撤销/重做”功能几乎无从实现:
// 反例:文档编辑器,直接在 Document 上执行操作
public class DocumentEditor {
private Document document;
public void type(String text) {
document.appendText(text);
// 操作已执行,无法撤销——没有记录"执行了什么操作"
}
public void delete(int start, int length) {
document.deleteText(start, length);
// 同样,操作发生后信息丢失
}
// 如果想实现撤销,只能记录整个文档的历史快照(原型模式)
// 这对大文档非常消耗内存
}快照式撤销的问题:对于大型文档(如几十 MB 的富文本),每次操作都存一份完整快照,内存消耗难以接受。命令模式提供了更精细的方案:只记录”操作本身”(如”在位置 50 插入了’hello’”),撤销时执行逆操作(“从位置 50 删除 5 个字符”)。
2.2 命令模式的结构
// 命令接口:封装一个可执行、可撤销的操作
public interface Command {
void execute(); // 执行操作
void undo(); // 撤销操作(执行逆操作)
}
// Receiver(接收者):真正执行操作的对象
public class TextDocument {
private final StringBuilder content = new StringBuilder();
public void insertText(int position, String text) {
content.insert(position, text);
}
public void deleteText(int position, int length) {
content.delete(position, position + length);
}
public String getContent() {
return content.toString();
}
}
// 具体命令一:插入文本(包含执行和撤销逻辑)
public class InsertTextCommand implements Command {
private final TextDocument document; // 接收者
private final int position;
private final String text;
public InsertTextCommand(TextDocument document, int position, String text) {
this.document = document;
this.position = position;
this.text = text;
}
@Override
public void execute() {
document.insertText(position, text);
}
@Override
public void undo() {
// 撤销插入 = 删除同样的文本
document.deleteText(position, text.length());
}
}
// 具体命令二:删除文本
public class DeleteTextCommand implements Command {
private final TextDocument document;
private final int position;
private final int length;
private String deletedText; // 记录被删除的内容(undo 时需要)
public DeleteTextCommand(TextDocument document, int position, int length) {
this.document = document;
this.position = position;
this.length = length;
}
@Override
public void execute() {
// 执行前保存被删除的内容
deletedText = document.getContent().substring(position, position + length);
document.deleteText(position, length);
}
@Override
public void undo() {
// 撤销删除 = 在原位置重新插入被删除的内容
document.insertText(position, deletedText);
}
}
// Invoker(调用者):管理命令历史,支持 Undo/Redo
public class CommandHistory {
private final Deque<Command> undoStack = new ArrayDeque<>(); // 已执行的命令
private final Deque<Command> redoStack = new ArrayDeque<>(); // 已撤销的命令
public void execute(Command command) {
command.execute();
undoStack.push(command);
redoStack.clear(); // 执行新命令后,清空重做栈
}
public void undo() {
if (undoStack.isEmpty()) return;
Command command = undoStack.pop();
command.undo();
redoStack.push(command); // 撤销的命令放入重做栈
}
public void redo() {
if (redoStack.isEmpty()) return;
Command command = redoStack.pop();
command.execute();
undoStack.push(command);
}
public boolean canUndo() { return !undoStack.isEmpty(); }
public boolean canRedo() { return !redoStack.isEmpty(); }
}
// 使用:命令的执行与撤销
TextDocument doc = new TextDocument();
CommandHistory history = new CommandHistory();
history.execute(new InsertTextCommand(doc, 0, "Hello")); // doc: "Hello"
history.execute(new InsertTextCommand(doc, 5, " World")); // doc: "Hello World"
history.execute(new DeleteTextCommand(doc, 5, 6)); // doc: "Hello"
history.undo(); // 撤销删除 → doc: "Hello World"
history.undo(); // 撤销插入 " World" → doc: "Hello"
history.redo(); // 重做插入 " World" → doc: "Hello World"2.3 命令模式的更多用途
用途一:操作队列(任务调度)
命令对象可以被序列化后放入消息队列,由消费者异步执行:
// 命令的序列化形式(用于消息队列传输)
public class CommandMessage {
private String commandType; // "INSERT_TEXT" / "DELETE_TEXT"
private Map<String, Object> params; // 命令参数
}
// 生产者:将操作封装为消息发送
messageQueue.send(new CommandMessage("INSERT_TEXT", Map.of(
"documentId", docId,
"position", 100,
"text", "New content"
)));
// 消费者:从消息恢复命令对象并执行
Command command = commandFactory.fromMessage(message);
command.execute();这就是 CQRS(命令查询责任分离)架构模式的理论基础——将”写操作”(Command)与”读操作”(Query)分离。
用途二:宏命令(Macro Command)
将多个命令组合成一个复合命令:
// 宏命令:组合多个命令,一次性执行或撤销
public class MacroCommand implements Command {
private final List<Command> commands;
public MacroCommand(List<Command> commands) {
this.commands = new ArrayList<>(commands);
}
@Override
public void execute() {
for (Command cmd : commands) {
cmd.execute();
}
}
@Override
public void undo() {
// 逆序撤销(后执行的先撤销)
ListIterator<Command> it = commands.listIterator(commands.size());
while (it.hasPrevious()) {
it.previous().undo();
}
}
}用途三:事务脚本
数据库事务可以看作一组命令的集合,事务提交即执行所有命令,事务回滚即撤销所有已执行的命令。
2.4 Java 函数式接口对命令模式的简化
当命令只需要”执行”(不需要撤销)时,Java 8 的 Runnable、Callable、Supplier 等函数式接口本质上就是命令接口,可以直接用 Lambda 代替:
// 简单命令(不需要 undo):直接用 Runnable
Runnable logCommand = () -> logger.log("Operation performed");
Runnable notifyCommand = () -> notificationService.notify(userId, "Done");
// 命令队列
Queue<Runnable> commandQueue = new LinkedList<>();
commandQueue.offer(logCommand);
commandQueue.offer(notifyCommand);
// 执行所有命令
while (!commandQueue.isEmpty()) {
commandQueue.poll().run();
}
// CompletableFuture 就是命令模式的异步版本
CompletableFuture.runAsync(logCommand)
.thenRun(notifyCommand);第 3 章 迭代器模式(Iterator)
3.1 动机:为什么需要统一的遍历接口
迭代器模式(Iterator Pattern)的定义:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
这个模式看起来如此简单,以至于很多人不理解它的价值——“不就是一个 for 循环吗?“要理解迭代器的真正价值,需要从两个维度思考:
维度一:封装内部结构
不同的数据结构有不同的遍历方式:数组通过下标遍历、链表通过 next 指针遍历、二叉树通过递归遍历、图通过 BFS/DFS 遍历。如果调用方直接依赖这些遍历机制,就与具体的数据结构耦合了。迭代器将遍历逻辑封装在内部,对外提供统一的 hasNext()/next() 接口,调用方不需要知道底层是什么数据结构。
维度二:统一的遍历协议
Java 的 for-each 语法(增强型 for 循环)能工作,依赖的正是 Iterable 接口和 Iterator 接口。任何实现了 Iterable<T> 的类都可以用 for (T item : collection) 语法遍历,这是 Java 语言级别对迭代器模式的直接支持。
3.2 Java 中迭代器的标准实现
来看一个自定义数据结构如何通过实现 Iterable 来支持 for-each:
// 自定义范围(Range)数据结构:表示 [start, end) 的整数范围
// 不实际存储所有数字,而是按需生成——迭代器的延迟计算能力
public class Range implements Iterable<Integer> {
private final int start;
private final int end;
private final int step;
public Range(int start, int end, int step) {
if (step <= 0) throw new IllegalArgumentException("Step must be positive");
this.start = start;
this.end = end;
this.step = step;
}
public Range(int start, int end) {
this(start, end, 1);
}
@Override
public Iterator<Integer> iterator() {
return new RangeIterator();
}
// 内部迭代器:封装遍历状态(当前位置)
private class RangeIterator implements Iterator<Integer> {
private int current = start;
@Override
public boolean hasNext() {
return current < end;
}
@Override
public Integer next() {
if (!hasNext()) throw new NoSuchElementException();
int value = current;
current += step; // 推进迭代状态
return value;
}
@Override
public void remove() {
throw new UnsupportedOperationException("Range is immutable");
}
}
}
// 使用:像内置集合一样用 for-each 遍历
Range range = new Range(1, 10, 2); // 1, 3, 5, 7, 9
for (int n : range) {
System.out.println(n);
}
// 也可以显式使用迭代器
Iterator<Integer> it = range.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}这里有一个重要的设计细节:Range 对象本身不存储任何整数,每次调用 next() 时按需计算下一个值。这是迭代器模式的一个重要优势——延迟计算(Lazy Evaluation),对于表示无限序列(如斐波那契数列)或非常大的集合尤为重要。
3.3 外部迭代器 vs 内部迭代器
Java 迭代器属于外部迭代器(External Iterator):调用方控制迭代过程(调用 hasNext()/next()),决定何时推进,何时停止。
Java 8 引入的 Stream API 提供了内部迭代器(Internal Iterator):调用方声明”对每个元素做什么”,迭代过程由框架控制:
// 外部迭代器(传统方式)
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
if (name.startsWith("A")) {
System.out.println(name.toUpperCase());
}
}
// 内部迭代器(Stream API)
names.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.forEach(System.out::println);两者的本质区别:
- 外部迭代器:调用方掌控”下一步做什么”,灵活但需要手动管理状态;
- 内部迭代器:框架掌控迭代,调用方只声明转换逻辑,更简洁,且天然支持并行化(
parallelStream())。
Stream API 的底层实现正是迭代器模式的扩展——Spliterator(可分割迭代器)是 Stream API 的核心,它在普通 Iterator 的基础上增加了分割能力,支持将一个大集合分割为多个子集合并行处理。
3.4 ConcurrentModificationException:迭代器的安全机制
Java 集合的迭代器有一个重要的安全机制:Fast-Fail——在迭代过程中,如果检测到集合被修改(增删元素),立即抛出 ConcurrentModificationException。
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
// 错误:在 for-each 中删除元素
for (String item : list) {
if ("b".equals(item)) {
list.remove(item); // 抛出 ConcurrentModificationException!
}
}
// 正确方式一:使用 Iterator 的 remove() 方法
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if ("b".equals(it.next())) {
it.remove(); // 通过迭代器自身删除,不触发 Fast-Fail
}
}
// 正确方式二:Java 8 removeIf()(内部迭代器,更简洁)
list.removeIf(item -> "b".equals(item));
// 正确方式三:Stream filter 创建新列表
List<String> filtered = list.stream()
.filter(item -> !"b".equals(item))
.collect(Collectors.toList());Fast-Fail 的实现原理:ArrayList 维护一个 modCount 字段,每次结构性修改(增删元素)都会递增 modCount。迭代器在创建时记录当前的 modCount 值(expectedModCount),每次调用 next() 时检查 modCount == expectedModCount,不一致则抛出异常。
ConcurrentModificationException 不保证一定抛出
Fast-Fail 是”尽力而为”的机制,不是严格的保证。在并发场景下,它可能检测不到所有的并发修改(取决于 happens-before 关系)。不应该依赖它来保证并发安全——如果需要线程安全的迭代,应使用
CopyOnWriteArrayList、ConcurrentHashMap等并发集合,或使用显式锁保护迭代区间。
第 4 章 三种模式的协同:Netty 中的综合应用
这三个模式在 Netty 的设计中都有体现,且以协同的方式工作:
责任链(ChannelPipeline):入站/出站事件沿 Handler 链传播,每个 Handler 决定是否继续传播。
命令模式(Task Queue):提交给 EventLoop 的所有任务(channel.writeAndFlush()、ctx.write() 等)都被封装为 Runnable 对象放入 taskQueue,由 EventLoop 线程从队列中取出逐一执行——这是命令模式在异步编程中的典型应用。
迭代器:在 ServerBootstrap 中配置多个 EventLoopGroup 时,Netty 内部用迭代器(Round-Robin 方式)选择下一个 EventLoop 来处理新连接,封装了 EventLoop 的分配逻辑。
总结
本篇完成了行为型模式的中间部分:
-
责任链模式将请求处理者组织成链,支持”找到合适处理者”和”所有处理者依次处理”两种形式。纯责任链(请求被一个处理者处理)适合审批流程;变体责任链(所有处理者都参与,包裹式前置/后置逻辑)是 Servlet Filter、Netty ChannelPipeline 的实现基础。链的动态构建使得处理逻辑可以灵活组合,新增/删除处理步骤不影响其他代码;
-
命令模式将操作封装为对象,赋予操作被传递、存储、排队和撤销的能力。Undo/Redo 的核心是维护命令历史栈,每个命令同时实现
execute()和undo()两个方法。宏命令(复合命令)将多个命令打包,支持事务式的批量执行和回滚。Java 8 的Runnable/Callable等函数式接口是”只需执行不需撤销”的轻量命令; -
迭代器模式通过统一的
hasNext()/next()接口封装数据结构的遍历逻辑,是 Javafor-each语法的基础。外部迭代器(传统Iterator)调用方控制遍历节奏;内部迭代器(Stream API)框架控制遍历,调用方声明转换逻辑,天然支持并行化。ConcurrentModificationException的 Fast-Fail 机制保护迭代过程,但不保证并发安全,需要使用并发集合或显式锁。
下一篇行为型模式的收尾:状态模式(替代复杂的状态机 if-else)、中介者模式(减少多对象的直接耦合)、备忘录模式(对象状态快照,比原型模式更适合”历史回放”)、访问者模式(在不修改类的情况下添加新操作):08 行为型模式(下)——状态、中介者、备忘录与访问者。
参考资料
- GoF,《Design Patterns: Elements of Reusable Object-Oriented Software》, 1994
- Norman Maurer & Marvin Wolfthal,《Netty in Action》(责任链模式在 ChannelPipeline 中的应用)
- Oracle Java Docs:
java.util.Iterator、java.util.Spliterator- Martin Fowler,《Patterns of Enterprise Application Architecture》(命令模式 / 事务脚本)
思考题
- 策略模式和模板方法模式都用于’算法可替换’。策略模式通过组合(持有策略接口的引用),模板方法通过继承(子类重写抽象方法)。Go 语言没有继承,因此只能使用策略模式。在 Java 中,什么时候应该优先选择策略模式而非模板方法?Lambda 表达式的引入是否使策略模式更加简洁?
- Spring 的
AbstractApplicationContext.refresh()是模板方法模式的经典实现——它定义了 IoC 容器初始化的骨架流程,子类(如ClassPathXmlApplicationContext)可以重写特定步骤。如果你需要在refresh()流程中插入一个自定义步骤(如加载远程配置),你应该重写模板方法还是使用 BeanFactoryPostProcessor?- 策略模式中,策略的选择通常由客户端决定。但在某些场景下,策略选择逻辑本身很复杂(如根据用户等级、地区、AB 测试分组选择不同的优惠策略)。这种’策略路由’逻辑应该放在哪里?直接用
if-else选择策略与使用’策略工厂 + 注解注册’方案相比,代码维护性有什么差异?