行为型设计模式¶
行为型模式关注对象之间如何协作、如何分配职责。它们描述的不只是对象的结构,更是运行时的交互方式:谁发起请求、谁处理请求、状态如何流转、算法如何替换。
GoF 的行为型模式共 11 种,可以再分为三类:
| 关注点 | 模式 |
|---|---|
| 对象间的通信与职责传递 | 责任链、命令、中介者、观察者 |
| 对象的状态与算法变化 | 状态、策略、模板方法、访问者 |
| 对集合与结构的遍历、解释、快照 | 迭代器、解释器、备忘录 |
责任链(Chain of Responsibility)¶
意图
将请求沿处理者链传递,直到有一个处理者处理它。发送者无需知道谁会处理。
适用场景¶
- 审批流、过滤器链、中间件;
- 多个对象可能处理同一个请求,但处理者不确定;
- 需要动态调整处理顺序。
结构¶
public abstract class Approver {
private Approver next;
public Approver setNext(Approver next) {
this.next = next;
return next; // 便于链式组装
}
public abstract void approve(String request, int days);
protected void forward(String request, int days) {
if (next != null) {
next.approve(request, days);
} else {
System.out.println("无人能处理: " + request);
}
}
}
public class TeamLead extends Approver {
@Override
public void approve(String request, int days) {
if (days <= 3) {
System.out.println("组长批准: " + request);
} else {
forward(request, days);
}
}
}
public class Manager extends Approver {
@Override
public void approve(String request, int days) {
if (days <= 7) {
System.out.println("经理批准: " + request);
} else {
forward(request, days);
}
}
}
public class Director extends Approver {
@Override
public void approve(String request, int days) {
System.out.println("总监批准: " + request);
}
}
// 组装链
Approver chain = new TeamLead();
chain.setNext(new Manager()).setNext(new Director());
chain.approve("年假", 5); // 经理批准
chain.approve("年假", 15); // 总监批准
权衡¶
- 优点:解耦发送者与处理者;可动态增删处理者;符合开闭原则。
- 缺点:请求可能无人处理;链过长影响性能;调试时难以追踪请求流向。
易错点¶
- 没有兜底处理者,请求被静默丢弃;
- 处理者之间互相依赖,形成隐式耦合;
- 链中出现环,导致无限循环。
命令(Command)¶
意图
将请求封装成对象,从而可以用不同请求对客户端参数化,并支持排队、记录日志、撤销。
结构¶
public interface Command {
void execute();
void undo();
}
// 接收者
public class Light {
public void on() { System.out.println("灯亮"); }
public void off() { System.out.println("灯灭"); }
}
// 具体命令
public class LightOnCommand implements Command {
private final Light light;
public LightOnCommand(Light light) { this.light = light; }
@Override public void execute() { light.on(); }
@Override public void undo() { light.off(); }
}
// 调用者(Invoker):不关心命令细节
public class RemoteControl {
private final Deque<Command> history = new ArrayDeque<>();
public void press(Command command) {
command.execute();
history.push(command);
}
public void undoLast() {
if (!history.isEmpty()) {
history.pop().undo();
}
}
}
// 使用
Light light = new Light();
RemoteControl remote = new RemoteControl();
remote.press(new LightOnCommand(light));
remote.undoLast(); // 灯灭
适用场景¶
- 需要撤销/重做;
- 需要把操作排队、延迟执行或异步执行;
- 需要记录操作日志、支持事务回滚;
- 需要把"触发者"与"执行者"解耦。
易错点¶
undo没有完整恢复状态(尤其是多步操作);- 命令对象持有可变上下文,导致重放结果不一致;
- 为简单调用强行套命令模式,增加无谓的类。
解释器(Interpreter)¶
意图
给定一个语言,定义它的文法表示,并定义一个解释器来解释语言中的句子。
适用场景¶
- 简单的领域特定语言(DSL)、规则表达式;
- 文法相对稳定、句子结构可递归组合;
- 复杂文法应使用解析器生成器(ANTLR 等),而不是手写解释器。
结构¶
public interface Expression {
int interpret(Map<String, Integer> context);
}
public record NumberExpression(int value) implements Expression {
@Override public int interpret(Map<String, Integer> context) { return value; }
}
public record VariableExpression(String name) implements Expression {
@Override
public int interpret(Map<String, Integer> context) {
Integer value = context.get(name);
if (value == null) {
throw new IllegalArgumentException("未定义的变量: " + name);
}
return value;
}
}
public record AddExpression(Expression left, Expression right) implements Expression {
@Override
public int interpret(Map<String, Integer> context) {
return left.interpret(context) + right.interpret(context);
}
}
public record MultiplyExpression(Expression left, Expression right) implements Expression {
@Override
public int interpret(Map<String, Integer> context) {
return left.interpret(context) * right.interpret(context);
}
}
// 表达式 (a + 10) * b
Expression expr = new MultiplyExpression(
new AddExpression(new VariableExpression("a"), new NumberExpression(10)),
new VariableExpression("b"));
int result = expr.interpret(Map.of("a", 5, "b", 3)); // 45
权衡¶
- 优点:文法易于扩展(新增一个表达式类即可);实现直观。
- 缺点:复杂文法会导致类爆炸;解释效率低;通常需要额外实现词法/语法分析。
易错点¶
- 用解释器实现本可以用配置或脚本引擎解决的问题;
- 没有处理运算符优先级,完全依赖手工组装语法树;
- 递归过深导致栈溢出。
迭代器(Iterator)¶
意图
提供一种方法顺序访问聚合对象中的元素,而不暴露其内部表示。
结构¶
public class NumberRange implements Iterable<Integer> {
private final int start;
private final int end;
public NumberRange(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public Iterator<Integer> iterator() {
return new Iterator<>() {
private int current = start;
@Override
public boolean hasNext() {
return current <= end;
}
@Override
public Integer next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return current++;
}
};
}
}
// for-each 自动使用迭代器
for (int n : new NumberRange(1, 5)) {
System.out.print(n + " ");
}
// 1 2 3 4 5
适用场景¶
- 集合的内部结构需要隐藏;
- 需要多种遍历方式(正序、逆序、过滤);
- 需要统一不同集合的遍历接口。
Java 中的迭代器¶
Iterator/Iterable/ for-each 是语言级支持;- 集合的
iterator()通常是 fail-fast 的:遍历过程中结构被修改会抛ConcurrentModificationException; ListIterator支持双向遍历和修改;Stream可以看作更高级的惰性遍历抽象。
易错点¶
- 在 for-each 中调用
remove(),应使用iterator.remove(); - 迭代器与集合的生命周期不一致,集合修改后继续使用旧迭代器;
- 自定义迭代器的
next()不检查边界。
中介者(Mediator)¶
意图
用一个中介对象封装一系列对象之间的交互,使对象之间不再显式互相引用,从而降低耦合。
结构¶
public interface ChatMediator {
void addUser(User user);
void send(String message, User sender);
}
public class ChatRoom implements ChatMediator {
private final List<User> users = new ArrayList<>();
@Override
public void addUser(User user) { users.add(user); }
@Override
public void send(String message, User sender) {
for (User user : users) {
if (user != sender) {
user.receive(sender.getName() + ": " + message);
}
}
}
}
public class User {
private final String name;
private final ChatMediator mediator;
public User(String name, ChatMediator mediator) {
this.name = name;
this.mediator = mediator;
mediator.addUser(this);
}
public String getName() { return name; }
public void send(String message) { mediator.send(message, this); }
public void receive(String message) {
System.out.println(name + " 收到 -> " + message);
}
}
// 使用
ChatMediator room = new ChatRoom();
User alice = new User("Alice", room);
User bob = new User("Bob", room);
alice.send("大家好");
如果没有中介者,每个用户都要持有其他所有用户的引用,关系数从 n 变成 n(n-1)/2。中介者把多对多变成一对多。
适用场景¶
- 多个对象之间存在复杂的网状引用;
- 交互逻辑集中管理比分散在对象中更清晰;
- 典型例子:聊天室、UI 对话框(控件之间通过对话框协调)、事件总线、微服务网关。
与外观的区别¶
- 外观是单向的:客户端 → 子系统,子系统不知道外观;
- 中介者是双向的:同事对象与中介者互相通信。
易错点¶
- 中介者承担过多逻辑,退化成"上帝对象";
- 所有交互都强制经过中介者,即使两个对象本可直接通信;
- 中介者成为单点故障或性能瓶颈。
备忘录(Memento)¶
意图
在不破坏封装的前提下,捕获并保存一个对象的内部状态,以便之后恢复。
结构¶
// 备忘录:对外不可变,只能由原发器读取
public record EditorSnapshot(String content, int cursor) {}
// 原发器:负责创建和恢复快照
public class Editor {
private String content = "";
private int cursor = 0;
public void type(String text) {
content += text;
cursor = content.length();
}
public EditorSnapshot save() {
return new EditorSnapshot(content, cursor);
}
public void restore(EditorSnapshot snapshot) {
this.content = snapshot.content();
this.cursor = snapshot.cursor();
}
@Override
public String toString() {
return content + " (cursor=" + cursor + ")";
}
}
// 管理者:只负责保存快照,不关心内容
public class History {
private final Deque<EditorSnapshot> snapshots = new ArrayDeque<>();
public void push(EditorSnapshot snapshot) {
snapshots.push(snapshot);
}
public EditorSnapshot pop() {
return snapshots.pop();
}
public boolean isEmpty() { return snapshots.isEmpty(); }
}
// 使用
Editor editor = new Editor();
History history = new History();
history.push(editor.save());
editor.type("Hello");
history.push(editor.save());
editor.type(" World");
editor.restore(history.pop());
System.out.println(editor); // Hello
适用场景¶
- 撤销/重做、事务回滚;
- 保存检查点、游戏存档;
- 需要快照但不想暴露对象内部结构。
权衡¶
- 优点:封装性好;状态恢复逻辑集中在原发器。
- 缺点:快照可能占用大量内存;频繁保存影响性能;需要处理深拷贝。
易错点¶
- 快照只做了浅拷贝,内部可变对象仍被共享;
- 无限保存快照导致内存泄漏,应限制历史长度或使用增量快照;
- 管理者直接修改备忘录内容,破坏封装。
观察者(Observer)¶
意图
定义对象间的一对多依赖:当一个对象状态改变时,所有依赖它的对象都自动收到通知。
结构¶
public interface Observer {
void update(double temperature, double humidity);
}
public interface Subject {
void register(Observer observer);
void unregister(Observer observer);
void notifyObservers();
}
public class WeatherStation implements Subject {
private final List<Observer> observers = new CopyOnWriteArrayList<>();
private double temperature;
private double humidity;
@Override
public void register(Observer observer) { observers.add(observer); }
@Override
public void unregister(Observer observer) { observers.remove(observer); }
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity);
}
}
public void setMeasurements(double temperature, double humidity) {
this.temperature = temperature;
this.humidity = humidity;
notifyObservers();
}
}
public class PhoneDisplay implements Observer {
@Override
public void update(double temperature, double humidity) {
System.out.printf("手机显示: %.1f°C, %.0f%%%n", temperature, humidity);
}
}
// 使用
WeatherStation station = new WeatherStation();
station.register(new PhoneDisplay());
station.setMeasurements(26.5, 60);
推模型 vs 拉模型¶
| 推模型 | 拉模型 | |
|---|---|---|
| 通知内容 | 直接把数据传给观察者 | 只通知"变了",观察者自己来取 |
| 耦合 | 观察者依赖数据结构 | 观察者依赖 Subject 接口 |
| 灵活性 | 数据变化时接口要改 | 观察者按需获取 |
Java 内置的 PropertyChangeSupport、java.util.Observable(已废弃)、Spring 的 ApplicationEvent、响应式框架的 Flow API 都属于观察者思想的演化。
易错点¶
- 通知过程中修改观察者列表,导致
ConcurrentModificationException(用CopyOnWriteArrayList或先拷贝); - 观察者未注销,造成内存泄漏(长生命周期的 Subject 持有短生命周期对象);
- 通知顺序被隐式依赖;
- 同步通知中观察者抛异常,导致后续观察者收不到通知——需要决定隔离策略;
- 观察者之间形成循环通知。
状态(State)¶
意图
允许对象在内部状态改变时改变它的行为,看起来像修改了它的类。
结构¶
public interface OrderState {
void pay(Order order);
void ship(Order order);
void cancel(Order order);
}
public class Order {
private OrderState state = new NewState();
private String id;
public Order(String id) { this.id = id; }
public void setState(OrderState state) { this.state = state; }
public void pay() { state.pay(this); }
public void ship() { state.ship(this); }
public void cancel() { state.cancel(this); }
}
public class NewState implements OrderState {
@Override
public void pay(Order order) {
System.out.println("支付成功");
order.setState(new PaidState());
}
@Override public void ship(Order order) {
throw new IllegalStateException("未支付不能发货");
}
@Override
public void cancel(Order order) {
System.out.println("订单已取消");
order.setState(new CancelledState());
}
}
public class PaidState implements OrderState {
@Override public void pay(Order order) {
throw new IllegalStateException("已支付,请勿重复支付");
}
@Override
public void ship(Order order) {
System.out.println("已发货");
order.setState(new ShippedState());
}
@Override
public void cancel(Order order) {
System.out.println("退款后取消");
order.setState(new CancelledState());
}
}
每个状态只处理自己允许的操作,非法操作在状态内部拒绝,避免出现散落各处的 if (status == ...)。
与策略的区别¶
两者结构几乎相同,区别在谁决定切换:
| 状态 | 策略 | |
|---|---|---|
| 切换时机 | 状态对象内部根据事件自动切换 | 由客户端显式选择 |
| 关注点 | 对象生命周期中的状态流转 | 同一任务的不同算法 |
| 对象关系 | 状态之间通常知道彼此 | 策略之间通常互不知道 |
| 典型场景 | 订单、工单、电梯 | 折扣、排序、压缩 |
易错点¶
- 状态类之间互相创建,形成强耦合;可以用状态工厂或枚举管理;
- 状态转换散落在多个方法里,缺少统一的状态机定义;
- 把状态数据放在 Context 里又让 State 直接修改,破坏封装。
策略(Strategy)¶
意图
定义一系列算法,把每个算法封装起来,并使它们可以互相替换。
结构¶
public interface DiscountStrategy {
BigDecimal apply(BigDecimal original);
}
public class NoDiscount implements DiscountStrategy {
@Override public BigDecimal apply(BigDecimal original) { return original; }
}
public class PercentageDiscount implements DiscountStrategy {
private final BigDecimal rate;
public PercentageDiscount(BigDecimal rate) { this.rate = rate; }
@Override
public BigDecimal apply(BigDecimal original) {
return original.multiply(BigDecimal.ONE.subtract(rate));
}
}
public class FullReduction implements DiscountStrategy {
private final BigDecimal threshold;
private final BigDecimal reduction;
public FullReduction(BigDecimal threshold, BigDecimal reduction) {
this.threshold = threshold;
this.reduction = reduction;
}
@Override
public BigDecimal apply(BigDecimal original) {
return original.compareTo(threshold) >= 0
? original.subtract(reduction)
: original;
}
}
// 上下文
public class Checkout {
private DiscountStrategy strategy = new NoDiscount();
public void setStrategy(DiscountStrategy strategy) {
this.strategy = strategy;
}
public BigDecimal total(BigDecimal original) {
return strategy.apply(original);
}
}
// 使用
Checkout checkout = new Checkout();
checkout.setStrategy(new PercentageDiscount(new BigDecimal("0.2")));
System.out.println(checkout.total(new BigDecimal("100"))); // 80
替代 if-else 的正确姿势¶
策略模式常被用来消除长 if-else,但不要机械化:
- 如果分支很少且稳定,
if-else更直观; - 如果分支对应不同算法且会持续增加,策略更合适;
- 用
Map<String, Strategy>或 Spring 注入可以避免新增策略时修改选择逻辑。
Java 8 之后,只有一个方法的策略接口可以用函数式接口 + Lambda 简化:
Map<String, UnaryOperator<BigDecimal>> discounts = Map.of(
"none", x -> x,
"vip", x -> x.multiply(new BigDecimal("0.9")),
"sale", x -> x.subtract(new BigDecimal("10"))
);
易错点¶
- 客户端必须了解各策略的差异,选择逻辑仍然散落在客户端;
- 策略需要访问大量上下文数据,导致参数列表膨胀(可以把上下文封装成对象传入);
- 策略对象有状态却被多个客户端共享。
模板方法(Template Method)¶
意图
在父类中定义算法骨架,把某些步骤延迟到子类实现,使子类可以在不改变算法结构的情况下重定义特定步骤。
结构¶
public abstract class DataImporter {
// 模板方法:定义流程,声明 final 防止子类改变骨架
public final void importData(String path) {
String raw = read(path);
if (!validate(raw)) {
throw new IllegalArgumentException("数据校验失败");
}
List<Record> records = parse(raw);
beforeSave(records); // 钩子,可选
save(records);
}
protected abstract String read(String path);
protected abstract List<Record> parse(String raw);
protected abstract void save(List<Record> records);
protected boolean validate(String raw) {
return raw != null && !raw.isBlank();
}
// 钩子方法:子类可选择性覆盖
protected void beforeSave(List<Record> records) {}
}
public class CsvImporter extends DataImporter {
@Override protected String read(String path) { /* 读文件 */ return ""; }
@Override protected boolean validate(String raw) { return !raw.isEmpty(); }
@Override protected List<Record> parse(String raw) { /* 解析 CSV */ return List.of(); }
@Override protected void save(List<Record> records) { /* 写数据库 */ }
}
好莱坞原则¶
"Don't call us, we'll call you." 父类控制流程,在合适的时机回调子类。子类只提供步骤实现,不控制整体流程。
Java 中的例子:
AbstractList、AbstractMap;InputStream.read()与子类实现的read(byte[], int, int);- JUnit 生命周期(
@BeforeEach等); - Servlet 的
service()调用doGet()/doPost()。
与策略的区别¶
| 模板方法 | 策略 | |
|---|---|---|
| 复用方式 | 继承,父类控制流程 | 组合,客户端选择算法 |
| 扩展点 | 覆盖步骤方法 | 实现整个算法接口 |
| 运行时变化 | 固定(编译期确定子类) | 可动态切换 |
| 耦合 | 子类依赖父类 | 上下文只依赖策略接口 |
两者可以结合:模板方法中的某个步骤委托给策略对象。
易错点¶
- 模板方法没有声明
final,子类覆盖后破坏流程; - 抽象方法过多,子类被迫实现不需要的步骤(违反 接口隔离原则);
- 父类调用子类方法时使用了子类尚未初始化的字段(构造器中调用模板方法是典型陷阱)。
访问者(Visitor)¶
意图
表示一个作用于某对象结构各元素的操作。它使你可以在不改变各元素类的前提下定义新操作。
结构¶
public interface ShapeVisitor<R> {
R visit(Circle circle);
R visit(Rectangle rectangle);
}
public sealed interface Shape permits Circle, Rectangle {
<R> R accept(ShapeVisitor<R> visitor);
}
public record Circle(double radius) implements Shape {
@Override
public <R> R accept(ShapeVisitor<R> visitor) {
return visitor.visit(this);
}
}
public record Rectangle(double width, double height) implements Shape {
@Override
public <R> R accept(ShapeVisitor<R> visitor) {
return visitor.visit(this);
}
}
// 新操作:计算面积
public class AreaVisitor implements ShapeVisitor<Double> {
@Override
public Double visit(Circle circle) {
return Math.PI * circle.radius() * circle.radius();
}
@Override
public Double visit(Rectangle rectangle) {
return rectangle.width() * rectangle.height();
}
}
// 新操作:导出 SVG
public class SvgVisitor implements ShapeVisitor<String> {
@Override
public String visit(Circle circle) {
return "<circle r=\"" + circle.radius() + "\"/>";
}
@Override
public String visit(Rectangle rectangle) {
return "<rect width=\"" + rectangle.width()
+ "\" height=\"" + rectangle.height() + "\"/>";
}
}
// 使用
List<Shape> shapes = List.of(new Circle(2), new Rectangle(3, 4));
ShapeVisitor<Double> area = new AreaVisitor();
double total = shapes.stream().mapToDouble(s -> s.accept(area)).sum();
双重分派¶
shape.accept(visitor) 先根据 shape 的运行时类型分派到具体 accept,再由 accept 调用 visitor.visit(this),根据参数类型分派到具体 visit。两次动态分派让操作与元素类型正确匹配。
适用场景¶
- 对象结构稳定,但需要频繁新增操作(编译器 AST、报表导出、序列化);
- 需要对一组异构对象执行统一操作;
- 不希望把每个操作都塞进元素类。
权衡¶
| 变化方向 | 访问者模式 |
|---|---|
| 新增操作 | 容易,新增 Visitor 即可 |
| 新增元素类型 | 困难,所有 Visitor 都要改 |
这与 开闭原则 形成取舍:访问者用"元素类型封闭"换取"操作开放"。
易错点¶
- 元素类型频繁增加时使用访问者,导致所有访问者反复修改;
- 访问者需要访问元素内部状态,破坏封装;
- 元素层次没有封闭(非
sealed),新增类型时编译器无法提醒。
如何选择¶
| 需求 | 推荐模式 |
|---|---|
| 请求需要多个处理者依次尝试 | 责任链 |
| 需要撤销、排队、记录操作 | 命令 |
| 简单 DSL / 规则表达式 | 解释器 |
| 隐藏集合内部结构,统一遍历 | 迭代器 |
| 对象间网状依赖需要收口 | 中介者 |
| 需要保存和恢复对象状态 | 备忘录 |
| 状态变化需要通知多个订阅者 | 观察者 |
| 对象行为随内部状态变化 | 状态 |
| 同一任务需要可替换的算法 | 策略 |
| 固定流程、部分步骤可变 | 模板方法 |
| 元素类型稳定、操作频繁增加 | 访问者 |
行为型模式的核心
它们几乎都在回答同一个问题:把"变化"放到哪里。责任链把处理者变成可扩展的链,策略把算法变成可替换的对象,状态把分支变成状态对象,观察者把依赖变成订阅关系。找到变化点,再选择模式,而不是反过来。
创建日期: 2026-10-23 19:51:40
广告
人要恰饭的嘛🤑🤑