跳转至

行为型补充 — 七种行为型设计模式

一句话记忆口诀:命令封装请求、迭代器遍历集合、中介者解耦交互、备忘录保存快照、状态消灭 if-else、访问者双分派、解释器解析语法。


一、命令模式(Command Pattern)

1. 引入:它解决了什么问题?

当需要将"请求"参数化、排队执行、支持撤销/重做时,直接调用方法无法满足:

// ❌ 反例:遥控器直接调用设备方法,无法撤销、无法排队
public class RemoteControl {
    public void pressButton(String device, String action) {
        if ("light".equals(device) && "on".equals(action)) {
            light.turnOn();
        } else if ("light".equals(device) && "off".equals(action)) {
            light.turnOff();
        } else if ("tv".equals(device) && "on".equals(action)) {
            tv.powerOn();
        }
        // 无法撤销上一步操作!无法记录操作历史!
    }
}

问题根因:请求的发送者与接收者直接耦合,无法对请求进行参数化、排队、撤销等操作。

2. 类比与定义

生活类比:餐厅点餐。顾客(发送者)不直接对厨师(接收者)喊菜,而是写在订单(命令对象)上交给服务员。订单可以排队、可以取消、可以记录历史。

命令模式将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录日志,以及支持可撤销的操作。

3. 原理与实现

// ===== 命令接口 =====
public interface Command {
    void execute();
    void undo(); // 支持撤销
}

// ===== 具体命令 =====
public class LightOnCommand implements Command {
    private Light light;
    public LightOnCommand(Light light) { this.light = light; }
    @Override
    public void execute() { light.turnOn(); }
    @Override
    public void undo() { light.turnOff(); } // 撤销 = 关灯
}

// ===== 调用者:遥控器 =====
public class RemoteControl {
    private Command lastCommand;
    private Deque<Command> history = new ArrayDeque<>();

    public void pressButton(Command command) {
        command.execute();
        history.push(command);
        lastCommand = command;
    }

    public void pressUndo() {
        if (lastCommand != null) {
            lastCommand.undo();
            history.pop();
        }
    }
}

// ===== 使用 =====
Light light = new Light();
RemoteControl remote = new RemoteControl();
remote.pressButton(new LightOnCommand(light)); // 开灯
remote.pressUndo(); // 撤销 → 关灯

4. 在 Spring / JDK 中的应用

框架/类 说明
Runnable / Callable 将任务封装为对象,提交给线程池执行
线程池 ThreadPoolExecutor 命令队列(BlockingQueue)存储待执行的任务
Spring Batch Step 每个 Step 是一个命令对象
javax.swing.Action Swing 中的动作命令

二、迭代器模式(Iterator Pattern)

1. 引入:它解决了什么问题?

不同的集合(数组、链表、树、哈希表)有不同的遍历方式,客户端需要了解每种集合的内部结构:

// ❌ 反例:遍历方式与集合类型强耦合
String[] array = {"a", "b", "c"};
for (int i = 0; i < array.length; i++) { /* 数组用索引 */ }

LinkedList<String> list = new LinkedList<>();
for (Node node = list.head; node != null; node = node.next) { /* 链表用指针 */ }
// 如果集合类型变了,遍历代码全部要改!

2. 类比与定义

生活类比:电视遥控器的"下一个频道"按钮。无论电视内部是用数组还是链表存储频道列表,用户只需要按"下一个"就能遍历所有频道。

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。

3. 原理与实现

// ===== 迭代器接口(JDK 已提供) =====
public interface Iterator<E> {
    boolean hasNext();
    E next();
}

// ===== 自定义集合 =====
public class BookShelf implements Iterable<Book> {
    private List<Book> books = new ArrayList<>();

    public void addBook(Book book) { books.add(book); }

    @Override
    public Iterator<Book> iterator() {
        return new BookIterator();
    }

    // 内部迭代器:封装遍历逻辑
    private class BookIterator implements Iterator<Book> {
        private int index = 0;
        @Override
        public boolean hasNext() { return index < books.size(); }
        @Override
        public Book next() { return books.get(index++); }
    }
}

// ===== 使用:统一的遍历方式 =====
BookShelf shelf = new BookShelf();
shelf.addBook(new Book("设计模式"));
shelf.addBook(new Book("重构"));
// for-each 底层就是迭代器模式
for (Book book : shelf) {
    System.out.println(book.getName());
}

4. 在 Spring / JDK 中的应用

框架/类 说明
java.util.Iterator JDK 标准迭代器接口
Iterable + for-each 语法糖,编译后使用 Iterator
Stream Java 8 内部迭代器,支持惰性求值
MyBatis Cursor 大数据量查询的流式迭代器
ResultSet JDBC 结果集遍历

三、中介者模式(Mediator Pattern)

1. 引入:它解决了什么问题?

当多个对象之间存在复杂的交互关系时,每个对象都需要持有其他对象的引用:

// ❌ 反例:聊天室中每个用户都直接引用其他用户
public class User {
    private List<User> contacts; // 每个用户都要维护联系人列表
    public void sendMessage(String msg, User to) {
        to.receive(msg); // 直接调用,N 个用户有 N*(N-1) 个关系
    }
}
// 新增一个用户,所有现有用户都要更新联系人列表!

问题根因:对象之间的交互形成网状结构,耦合度极高,难以维护和扩展。

2. 类比与定义

生活类比:机场塔台。飞机之间不直接通信(否则 100 架飞机有 4950 条通信线路),而是统一通过塔台(中介者)协调起降。

中介者模式用一个中介对象来封装一系列对象之间的交互,使各对象不需要显式地相互引用,从而使其耦合松散。

3. 原理与实现

// ===== 中介者接口 =====
public interface ChatRoom {
    void sendMessage(String message, User sender);
    void addUser(User user);
}

// ===== 具体中介者 =====
public class ChatRoomImpl implements ChatRoom {
    private List<User> users = new ArrayList<>();

    @Override
    public void addUser(User user) { users.add(user); }

    @Override
    public void sendMessage(String message, User sender) {
        // 中介者负责将消息转发给其他所有用户
        users.stream()
             .filter(u -> u != sender)
             .forEach(u -> u.receive(message, sender.getName()));
    }
}

// ===== 同事类 =====
public class User {
    private String name;
    private ChatRoom chatRoom; // 只依赖中介者,不依赖其他 User

    public User(String name, ChatRoom chatRoom) {
        this.name = name;
        this.chatRoom = chatRoom;
        chatRoom.addUser(this);
    }

    public void send(String message) {
        chatRoom.sendMessage(message, this); // 通过中介者发送
    }

    public void receive(String message, String from) {
        System.out.printf("[%s] 收到来自 %s 的消息: %s%n", name, from, message);
    }
}

4. 在 Spring / JDK 中的应用

框架/类 说明
Spring ApplicationEventPublisher 事件发布者作为中介,解耦事件生产者和消费者
MQ(Kafka/RabbitMQ) 消息队列作为中介,解耦生产者和消费者
java.util.Timer 定时器作为中介,协调多个 TimerTask
Spring MVC DispatcherServlet 中央调度器,协调 Controller、ViewResolver 等

四、备忘录模式(Memento Pattern)

1. 引入:它解决了什么问题?

需要保存对象的历史状态以支持撤销/回滚,但又不想暴露对象的内部实现细节。

2. 类比与定义

生活类比:游戏存档。玩家可以在关键节点保存进度(创建备忘录),失败后读取存档恢复到之前的状态,而不需要了解游戏内部的数据结构。

备忘录模式在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后恢复。

3. 原理与实现

// ===== 备忘录:保存状态快照 =====
public class EditorMemento {
    private final String content;
    private final int cursorPosition;
    private final LocalDateTime timestamp;

    public EditorMemento(String content, int cursorPosition) {
        this.content = content;
        this.cursorPosition = cursorPosition;
        this.timestamp = LocalDateTime.now();
    }
    // 只提供 getter,不提供 setter(不可变)
    public String getContent() { return content; }
    public int getCursorPosition() { return cursorPosition; }
}

// ===== 发起人:文本编辑器 =====
public class TextEditor {
    private String content = "";
    private int cursorPosition = 0;

    public void type(String text) {
        content += text;
        cursorPosition = content.length();
    }

    // 创建备忘录(保存当前状态)
    public EditorMemento save() {
        return new EditorMemento(content, cursorPosition);
    }

    // 从备忘录恢复状态
    public void restore(EditorMemento memento) {
        this.content = memento.getContent();
        this.cursorPosition = memento.getCursorPosition();
    }
}

// ===== 管理者:历史记录管理 =====
public class History {
    private Deque<EditorMemento> snapshots = new ArrayDeque<>();

    public void push(EditorMemento memento) { snapshots.push(memento); }
    public EditorMemento pop() { return snapshots.pop(); }
}

// ===== 使用 =====
TextEditor editor = new TextEditor();
History history = new History();

editor.type("Hello");
history.push(editor.save()); // 保存状态

editor.type(" World");
history.push(editor.save()); // 保存状态

editor.restore(history.pop()); // 撤销 → 回到 "Hello World"
editor.restore(history.pop()); // 撤销 → 回到 "Hello"

4. 在 Spring / JDK 中的应用

框架/类 说明
数据库 Undo Log 事务回滚时恢复数据的历史状态
Serializable 序列化就是保存对象状态的一种方式
Spring @Transactional 回滚 事务失败时恢复到事务开始前的状态
Git 版本控制 每次 commit 就是一个备忘录

五、状态模式(State Pattern)

1. 引入:它解决了什么问题?

当对象的行为随内部状态改变而改变时,代码中充满状态判断的 if-else:

// ❌ 反例:订单状态判断的 if-else 地狱
public class Order {
    private String state; // "CREATED", "PAID", "SHIPPED", "COMPLETED"

    public void nextStep() {
        if ("CREATED".equals(state)) {
            // 待支付 → 已支付
            pay();
            state = "PAID";
        } else if ("PAID".equals(state)) {
            // 已支付 → 已发货
            ship();
            state = "SHIPPED";
        } else if ("SHIPPED".equals(state)) {
            // 已发货 → 已完成
            complete();
            state = "COMPLETED";
        }
        // 每新增一个状态,所有方法都要加 else if!
    }
}

2. 类比与定义

生活类比:自动售货机。投币前只能投币,投币后可以选商品或退币,出货后回到初始状态。每个状态下的可用操作不同,状态机自动管理状态转换。

状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。

3. 原理与实现

// ===== 状态接口 =====
public interface OrderState {
    void handle(OrderContext context);
    String getStateName();
}

// ===== 具体状态 =====
public class CreatedState implements OrderState {
    @Override
    public void handle(OrderContext context) {
        System.out.println("订单已创建,执行支付...");
        context.setState(new PaidState()); // 状态转换
    }
    @Override
    public String getStateName() { return "待支付"; }
}

public class PaidState implements OrderState {
    @Override
    public void handle(OrderContext context) {
        System.out.println("订单已支付,执行发货...");
        context.setState(new ShippedState());
    }
    @Override
    public String getStateName() { return "已支付"; }
}

public class ShippedState implements OrderState {
    @Override
    public void handle(OrderContext context) {
        System.out.println("订单已发货,确认收货...");
        context.setState(new CompletedState());
    }
    @Override
    public String getStateName() { return "已发货"; }
}

// ===== 上下文 =====
public class OrderContext {
    private OrderState state;
    public OrderContext() { this.state = new CreatedState(); }
    public void setState(OrderState state) { this.state = state; }
    public void nextStep() { state.handle(this); }
    public String getCurrentState() { return state.getStateName(); }
}

// ===== 使用 =====
OrderContext order = new OrderContext();
order.nextStep(); // 待支付 → 已支付
order.nextStep(); // 已支付 → 已发货
order.nextStep(); // 已发货 → 已完成

4. 状态模式 vs 策略模式

对比维度 状态模式 策略模式
切换方式 状态自动转换(内部驱动) 客户端主动选择(外部驱动)
关注点 对象在不同状态下的行为差异 算法的可替换性
状态感知 状态对象知道下一个状态是什么 策略对象不知道其他策略
典型场景 订单状态机、TCP 连接状态 排序算法选择、支付方式选择

六、访问者模式(Visitor Pattern)

1. 引入:它解决了什么问题?

当需要对一个对象结构中的元素执行多种不同且不相关的操作时,把操作放在元素类中会导致类职责膨胀:

// ❌ 反例:每新增一种操作,所有元素类都要修改
public class Circle {
    public double area() { /* 计算面积 */ }
    public String toJson() { /* 序列化 */ }
    public void render() { /* 渲染 */ }
    // 新增"导出 SVG"操作?Circle、Rectangle、Triangle 全部要改!
}

2. 类比与定义

生活类比:税务审计。公司的各个部门(元素)结构稳定,但审计方式(访问者)经常变化。新增一种审计方式时,不需要修改部门结构,只需要新增一个审计员。

访问者模式将数据结构数据操作分离,在不修改数据结构的前提下定义新的操作。

3. 原理与实现

// ===== 访问者接口 =====
public interface ShapeVisitor {
    void visit(Circle circle);
    void visit(Rectangle rectangle);
}

// ===== 元素接口 =====
public interface Shape {
    void accept(ShapeVisitor visitor); // 双分派的关键
}

// ===== 具体元素 =====
public class Circle implements Shape {
    private double radius;
    public Circle(double radius) { this.radius = radius; }
    public double getRadius() { return radius; }
    @Override
    public void accept(ShapeVisitor visitor) {
        visitor.visit(this); // 第二次分派:根据 this 的类型调用对应方法
    }
}

// ===== 具体访问者:计算面积 =====
public class AreaCalculator implements ShapeVisitor {
    private double totalArea = 0;
    @Override
    public void visit(Circle circle) {
        totalArea += Math.PI * circle.getRadius() * circle.getRadius();
    }
    @Override
    public void visit(Rectangle rect) {
        totalArea += rect.getWidth() * rect.getHeight();
    }
    public double getTotalArea() { return totalArea; }
}

// ===== 具体访问者:导出 JSON =====
public class JsonExporter implements ShapeVisitor {
    private List<String> jsonList = new ArrayList<>();
    @Override
    public void visit(Circle circle) {
        jsonList.add("{\"type\":\"circle\",\"radius\":" + circle.getRadius() + "}");
    }
    @Override
    public void visit(Rectangle rect) {
        jsonList.add("{\"type\":\"rect\",\"w\":" + rect.getWidth() + "}");
    }
}

// ===== 使用:新增操作不修改元素类 =====
List<Shape> shapes = List.of(new Circle(5), new Rectangle(3, 4));
AreaCalculator calc = new AreaCalculator();
shapes.forEach(s -> s.accept(calc));
System.out.println("总面积: " + calc.getTotalArea());

4. 在 Spring / JDK 中的应用

框架/类 说明
编译器 AST 遍历 对语法树节点执行不同操作(类型检查、代码生成)
Spring BeanDefinitionVisitor 访问并处理 BeanDefinition 中的属性
FileVisitor (NIO) 遍历文件树时执行不同操作

5. 适用条件与局限

  • 适用:数据结构稳定(元素类型不常变),但操作经常变化
  • 不适用:元素类型经常新增(每新增一种元素,所有 Visitor 都要改)

七、解释器模式(Interpreter Pattern)

1. 引入:它解决了什么问题?

当需要解释执行一种特定的"语言"或"表达式"时,硬编码解析逻辑会导致代码难以扩展。

2. 类比与定义

生活类比:翻译官。不同语言(表达式)有不同的语法规则,翻译官(解释器)按照语法规则逐步解析并翻译。

解释器模式给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

3. 原理与实现

// ===== 抽象表达式 =====
public interface Expression {
    int interpret();
}

// ===== 终结符表达式:数字 =====
public class NumberExpression implements Expression {
    private int number;
    public NumberExpression(int number) { this.number = number; }
    @Override
    public int interpret() { return number; }
}

// ===== 非终结符表达式:加法 =====
public class AddExpression implements Expression {
    private Expression left, right;
    public AddExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }
    @Override
    public int interpret() {
        return left.interpret() + right.interpret();
    }
}

// ===== 非终结符表达式:乘法 =====
public class MultiplyExpression implements Expression {
    private Expression left, right;
    public MultiplyExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }
    @Override
    public int interpret() {
        return left.interpret() * right.interpret();
    }
}

// ===== 使用:构建表达式树并解释执行 =====
// 表达式:(3 + 5) * 2
Expression expr = new MultiplyExpression(
    new AddExpression(new NumberExpression(3), new NumberExpression(5)),
    new NumberExpression(2)
);
System.out.println(expr.interpret()); // 输出 16

4. 在 Spring / JDK 中的应用

框架/类 说明
Spring EL (SpEL) #{user.name} 表达式解析
正则表达式 Pattern 正则语法的解释执行
MyBatis 动态 SQL <if><where> 等标签的解析
java.text.Format 日期/数字格式化表达式

5. 适用条件与局限

  • 适用:语法简单、效率要求不高的场景(如配置解析、规则引擎)
  • 不适用:复杂语法(应使用专业的解析器生成工具如 ANTLR)

八、七种模式对比总结

模式 核心思想 解决的问题 关键词
命令模式 封装请求 请求需要排队/撤销/记录 请求对象化、Undo
迭代器模式 统一遍历 不同集合遍历方式不一致 hasNext/next
中介者模式 集中交互 对象间网状耦合 星型拓扑
备忘录模式 保存快照 需要撤销/回滚到历史状态 状态快照、Undo Log
状态模式 状态驱动 行为随状态变化的 if-else 状态机、自动转换
访问者模式 分离操作 数据结构稳定但操作多变 双分派
解释器模式 解析语法 需要解释执行特定语言 表达式树、文法

复习检验标准:能否口述"这个模式解决了什么问题?不用它会怎样?Spring 中哪里用到了?"