行为型模式(下)——状态、中介者、备忘录与访问者
摘要
行为型模式的最后一批包含四个各具特色的模式。状态模式(State) 解决的是”对象行为随内部状态变化而变化”的问题——当一个对象有多种状态,且在不同状态下对同一消息的响应完全不同时,用 if-else / switch 管理状态跳转会形成难以维护的”状态机泥潭”;状态模式将每种状态封装为独立的状态类,状态迁移逻辑分散到各状态内部,让状态机变得清晰可扩展。中介者模式(Mediator) 解决”多个对象之间网状依赖”的问题——当系统中多个组件都互相引用时,任何一个组件的变化都会波及所有其他组件;中介者引入一个中间人统一协调,将网状依赖简化为星型依赖。备忘录模式(Memento) 解决”对象状态的保存与恢复”问题——在不暴露对象内部实现细节的前提下,捕获对象的某个时刻的状态,并能在之后恢复到这个状态;分析备忘录与命令模式在撤销功能上的选型差异。访问者模式(Visitor) 解决”在不修改类的前提下为类添加新操作”的问题——它是双分派(Double Dispatch)技术的落地,适合类层次结构稳定但操作频繁扩展的场景;分析为什么 Java 的模式匹配(switch expressions with pattern)正在让传统访问者模式部分过时。
第 1 章 状态模式(State)
1.1 动机:状态机的 if-else 地狱
状态模式(State Pattern)的动机来自于一个非常常见的设计问题。当一个对象的行为依赖于它的状态,且状态会在运行时发生变化时,最直觉的做法是在每个方法里用条件语句判断当前状态:
想象一个电商系统中的订单状态机,订单有多种状态:待付款(PENDING)、已付款(PAID)、已发货(SHIPPED)、已完成(COMPLETED)、已取消(CANCELLED)。不同状态下的操作逻辑完全不同:
// 反例:用 if-else 管理状态逻辑,随状态增多急速膨胀
public class Order {
private OrderStatus status;
public void pay() {
if (status == OrderStatus.PENDING) {
// 从待付款变为已付款
this.status = OrderStatus.PAID;
notifyWarehouse();
} else if (status == OrderStatus.PAID) {
throw new IllegalStateException("Order already paid");
} else if (status == OrderStatus.SHIPPED) {
throw new IllegalStateException("Cannot pay a shipped order");
} else if (status == OrderStatus.COMPLETED) {
throw new IllegalStateException("Order already completed");
} else if (status == OrderStatus.CANCELLED) {
throw new IllegalStateException("Cannot pay a cancelled order");
}
}
public void ship() {
if (status == OrderStatus.PAID) {
this.status = OrderStatus.SHIPPED;
notifyLogistics();
} else if (status == OrderStatus.PENDING) {
throw new IllegalStateException("Cannot ship unpaid order");
} else if (status == OrderStatus.SHIPPED) {
throw new IllegalStateException("Order already shipped");
}
// ... 更多 else if
}
public void complete() {
if (status == OrderStatus.SHIPPED) {
this.status = OrderStatus.COMPLETED;
triggerAfterSaleProcess();
}
// ... 更多 else if
}
public void cancel() {
if (status == OrderStatus.PENDING || status == OrderStatus.PAID) {
this.status = OrderStatus.CANCELLED;
if (status == OrderStatus.PAID) {
initiateRefund();
}
} else {
throw new IllegalStateException("Cannot cancel: " + status);
}
}
}这段代码的问题:
- 条件语句爆炸:5 种状态 × 4 种操作,有效代码被 20 多个 if-else 分支淹没;
- 状态跳转逻辑散落:想知道”从哪些状态可以跳转到 SHIPPED”,需要翻遍所有方法;
- 扩展困难:新增”退款中(REFUNDING)“状态,需要修改所有方法,极易遗漏;
- 测试困难:单个状态的行为无法独立测试,必须通过
Order对象的全量测试覆盖。
1.2 状态模式的结构
状态模式将每种状态的行为封装到独立的状态类中,Order 对象通过委托给当前状态对象来执行操作:
// 状态接口:定义所有状态下可能被调用的操作
public interface OrderState {
void pay(Order order);
void ship(Order order);
void complete(Order order);
void cancel(Order order);
}
// 具体状态一:待付款状态
public class PendingState implements OrderState {
@Override
public void pay(Order order) {
// 在待付款状态下,支付是合法操作
order.setState(new PaidState()); // 迁移到已付款状态
order.notifyWarehouse();
}
@Override
public void ship(Order order) {
throw new IllegalStateException("Cannot ship: order is not paid yet");
}
@Override
public void complete(Order order) {
throw new IllegalStateException("Cannot complete: order is not paid");
}
@Override
public void cancel(Order order) {
order.setState(new CancelledState()); // 待付款可以直接取消,无需退款
}
}
// 具体状态二:已付款状态
public class PaidState implements OrderState {
@Override
public void pay(Order order) {
throw new IllegalStateException("Order already paid");
}
@Override
public void ship(Order order) {
order.setState(new ShippedState()); // 迁移到已发货状态
order.notifyLogistics();
}
@Override
public void complete(Order order) {
throw new IllegalStateException("Cannot complete: order not shipped yet");
}
@Override
public void cancel(Order order) {
order.setState(new CancelledState());
order.initiateRefund(); // 已付款取消需要退款
}
}
// 具体状态三:已发货状态
public class ShippedState implements OrderState {
@Override
public void pay(Order order) { throw new IllegalStateException("Already paid"); }
@Override
public void ship(Order order) { throw new IllegalStateException("Already shipped"); }
@Override
public void complete(Order order) {
order.setState(new CompletedState());
order.triggerAfterSaleProcess();
}
@Override
public void cancel(Order order) {
throw new IllegalStateException("Cannot cancel shipped order; request return instead");
}
}
// 已取消状态:所有操作都抛出异常(终态)
public class CancelledState implements OrderState {
private final String reason = "Order is cancelled";
@Override public void pay(Order order) { throw new IllegalStateException(reason); }
@Override public void ship(Order order) { throw new IllegalStateException(reason); }
@Override public void complete(Order order) { throw new IllegalStateException(reason); }
@Override public void cancel(Order order) { throw new IllegalStateException("Already cancelled"); }
}
// Context(上下文):Order 本身只持有当前状态,委托给状态对象执行
public class Order {
private OrderState state; // 当前状态
private final String id;
public Order(String id) {
this.id = id;
this.state = new PendingState(); // 初始状态
}
// 所有业务方法直接委托给状态对象
public void pay() { state.pay(this); }
public void ship() { state.ship(this); }
public void complete() { state.complete(this); }
public void cancel() { state.cancel(this); }
// package-private 或 protected:允许状态对象修改当前状态
void setState(OrderState newState) {
this.state = newState;
}
// 状态对象需要调用的动作
void notifyWarehouse() { ... }
void notifyLogistics() { ... }
void initiateRefund() { ... }
void triggerAfterSaleProcess() { ... }
}现在新增”退款中”状态,只需要新增一个 RefundingState 类,修改 CancelledState 和 PaidState 中涉及退款的状态迁移逻辑——其他状态类完全不受影响。
1.3 状态模式的两种状态迁移方式
状态迁移逻辑可以放在两个地方,各有优缺点:
方式一:状态迁移由状态类自己负责(上面示例的做法)
状态类在执行操作时直接调用 order.setState(new NextState())。优点是状态类高内聚(它知道自己执行什么操作后会到哪个状态);缺点是状态类之间存在间接耦合(PaidState 需要知道 ShippedState 的存在)。
方式二:状态迁移由 Context 负责
状态类只执行操作,返回执行结果;Order 根据结果决定下一个状态。优点是状态类之间完全独立;缺点是 Order 需要包含状态迁移图的知识。
在复杂的工作流系统(如 Activiti、Flowable 等工作流引擎)中,通常用外部配置文件(XML/JSON)描述状态转换图,Context 根据配置执行迁移,这样状态和状态之间的转换关系完全分离。
第 2 章 中介者模式(Mediator)
2.1 动机:网状依赖的复杂性爆炸
中介者模式(Mediator Pattern)解决的问题是:当系统中多个类相互依赖、直接引用时,任何一个类的修改都会波及多个其他类,形成难以维护的网状依赖。
典型场景是 GUI 对话框:一个对话框中有多个 UI 控件(文本框、下拉选择框、按钮、标签等),它们之间有复杂的联动关系——选择了某个选项后,其他控件的状态要相应变化(如选择”按月付款”后,隐藏”一次性优惠”文本框,显示”月付金额”字段)。
如果控件之间直接互相引用:
Button → TextField
Button → ComboBox
Button → Label
TextField → Button
TextField → ComboBox
ComboBox → TextField
ComboBox → Label
...
这是一个网状结构,M 个控件之间最多有 M×(M-1) 条依赖关系。修改任何一个控件的行为,都需要了解它与所有其他控件的交互逻辑,这就是”网状依赖”的维护噩梦。
2.2 中介者模式的结构
中介者引入一个中间对象(通常是 Dialog 或 Form 类本身),所有控件不再直接互相引用,而是通过中介者进行通信:
// 中介者接口:协调各组件间的通信
public interface DialogMediator {
void notify(UIComponent sender, String event);
}
// 抽象组件:持有中介者引用
public abstract class UIComponent {
protected DialogMediator mediator;
public UIComponent(DialogMediator mediator) {
this.mediator = mediator;
}
// 发生某个事件时,通知中介者
protected void trigger(String event) {
if (mediator != null) {
mediator.notify(this, event);
}
}
}
// 具体组件:文本框
public class TextField extends UIComponent {
private String text = "";
public void setText(String text) {
this.text = text;
trigger("textChanged"); // 通知中介者,文本发生了变化
}
public String getText() { return text; }
public void setEnabled(boolean enabled) { ... }
public void setVisible(boolean visible) { ... }
}
// 具体组件:下拉选择框
public class ComboBox extends UIComponent {
private String selectedItem = "";
public void select(String item) {
this.selectedItem = item;
trigger("selectionChanged"); // 通知中介者
}
public String getSelectedItem() { return selectedItem; }
}
// 具体组件:按钮
public class Button extends UIComponent {
private boolean enabled = true;
public void click() {
if (enabled) trigger("clicked");
}
public void setEnabled(boolean enabled) { this.enabled = enabled; }
}
// 具体中介者:订单表单对话框
// 它了解所有组件,并定义它们之间的交互规则
public class OrderFormDialog implements DialogMediator {
private final ComboBox paymentTypeBox;
private final TextField monthlyAmountField;
private final TextField oneTimeDiscountField;
private final Button submitButton;
public OrderFormDialog() {
// 创建所有组件,并将自己作为中介者传入
paymentTypeBox = new ComboBox(this);
monthlyAmountField = new TextField(this);
oneTimeDiscountField = new TextField(this);
submitButton = new Button(this);
// 初始状态
monthlyAmountField.setVisible(false);
oneTimeDiscountField.setVisible(true);
}
// 所有联动逻辑集中在这里,清晰可读
@Override
public void notify(UIComponent sender, String event) {
if (sender == paymentTypeBox && "selectionChanged".equals(event)) {
String type = paymentTypeBox.getSelectedItem();
if ("monthly".equals(type)) {
monthlyAmountField.setVisible(true);
oneTimeDiscountField.setVisible(false);
} else {
monthlyAmountField.setVisible(false);
oneTimeDiscountField.setVisible(true);
}
}
if (sender == submitButton && "clicked".equals(event)) {
// 提交表单
processOrder();
}
// 如果必填字段都填了,启用提交按钮
boolean canSubmit = !monthlyAmountField.getText().isEmpty()
|| !oneTimeDiscountField.getText().isEmpty();
submitButton.setEnabled(canSubmit);
}
private void processOrder() { ... }
}所有联动规则集中在 OrderFormDialog.notify() 中,UI 组件之间完全解耦——TextField 不知道 ComboBox 的存在,反之亦然。修改联动规则只需修改 OrderFormDialog。
2.3 中介者的真实案例:Spring MVC 的 DispatcherServlet
DispatcherServlet 在 Spring MVC 中扮演的正是中介者角色:HandlerMapping、HandlerAdapter、ViewResolver、HandlerExceptionResolver 等组件不互相引用,而是都与 DispatcherServlet 通信。
DispatcherServlet 协调这些组件:收到请求 → 用 HandlerMapping 找到 Handler → 用 HandlerAdapter 执行 Handler → 用 ViewResolver 解析视图 → 渲染响应。各组件只需关心自己的职责,而不需要了解整个处理流程——这是中介者模式在框架设计中的典型体现。
第 3 章 备忘录模式(Memento)
3.1 动机:状态保存与恢复的封装性挑战
备忘录模式(Memento Pattern)解决的问题是:需要保存一个对象的内部状态,以便之后可以恢复到这个状态,同时不暴露对象的内部实现细节。
备忘录模式与前面讨论的命令模式撤销有本质区别:
| 维度 | 命令模式撤销 | 备忘录模式 |
|---|---|---|
| 存储内容 | 操作本身(执行了什么) | 状态快照(某时刻的数据值) |
| 撤销方式 | 执行逆操作(数学上的反函数) | 恢复保存的快照(直接覆盖) |
| 适用场景 | 操作有明确的逆操作(插入↔删除) | 状态复杂,逆操作难以定义 |
| 内存消耗 | 低(只存操作描述) | 高(存完整状态,有时很大) |
| 多步回退 | 容易(维护命令历史栈) | 容易(维护快照历史栈) |
当对象状态很复杂、操作没有明确逆操作时(如绘图应用的”任意变形”),备忘录模式比命令模式更合适。
3.2 备忘录模式的结构
备忘录模式的关键设计是:外部代码可以持有备忘录对象,但不能读取或修改备忘录内部的状态——只有原始对象(Originator)能与备忘录的内部通信。这保证了封装性:外部代码只是在”保管”一个不透明的快照,不会因为访问备忘录而了解到对象的内部实现。
// Originator(原发器):需要保存/恢复状态的对象
public class GameCharacter {
private String name;
private int level;
private int health;
private int mana;
private String location;
private List<String> inventory;
// 创建备忘录(保存当前状态快照)
public GameMemento save() {
return new GameMemento(level, health, mana, location, new ArrayList<>(inventory));
}
// 从备忘录恢复状态
public void restore(GameMemento memento) {
this.level = memento.getLevel();
this.health = memento.getHealth();
this.mana = memento.getMana();
this.location = memento.getLocation();
this.inventory = new ArrayList<>(memento.getInventory());
}
// 游戏操作
public void takeDamage(int damage) {
this.health = Math.max(0, this.health - damage);
}
public void moveToLocation(String loc) {
this.location = loc;
}
public void addItem(String item) {
this.inventory.add(item);
}
}
// Memento(备忘录):存储原发器的内部状态快照
// 通常设计为不可变对象(所有字段 final,无 setter)
public final class GameMemento {
private final int level;
private final int health;
private final int mana;
private final String location;
private final List<String> inventory;
// package-private 构造器:只有同包的 GameCharacter 可以创建
GameMemento(int level, int health, int mana,
String location, List<String> inventory) {
this.level = level;
this.health = health;
this.mana = mana;
this.location = location;
this.inventory = Collections.unmodifiableList(new ArrayList<>(inventory));
}
// package-private getter:只有 GameCharacter 可以读取
int getLevel() { return level; }
int getHealth() { return health; }
int getMana() { return mana; }
String getLocation() { return location; }
List<String> getInventory() { return inventory; }
// 公开的展示方法(供存档界面显示)
@Override
public String toString() {
return String.format("SavePoint[Lv.%d, HP:%d, MP:%d, @%s]",
level, health, mana, location);
}
}
// Caretaker(管理者):保管备忘录历史,但不操作备忘录内部
public class GameSaveManager {
private final Deque<GameMemento> savePoints = new ArrayDeque<>();
public void saveGame(GameCharacter character) {
savePoints.push(character.save()); // 保存当前状态
System.out.println("Game saved: " + savePoints.peek());
}
public void loadLastSave(GameCharacter character) {
if (savePoints.isEmpty()) {
System.out.println("No save point available");
return;
}
GameMemento memento = savePoints.pop();
character.restore(memento); // 恢复状态
System.out.println("Game loaded: " + memento);
}
public int getSaveCount() { return savePoints.size(); }
}
// 使用
GameCharacter hero = new GameCharacter("Archer", 10, 500, 300, "Forest");
GameSaveManager saveManager = new GameSaveManager();
saveManager.saveGame(hero); // 存档点 1
hero.moveToLocation("Dark Dungeon");
hero.takeDamage(400); // HP 降到 100
hero.addItem("Cursed Sword");
saveManager.saveGame(hero); // 存档点 2(在地下城,HP 100)
hero.takeDamage(200); // 游戏over,HP 降到 0
saveManager.loadLastSave(hero); // 读取存档点 2,恢复到 HP 100
saveManager.loadLastSave(hero); // 读取存档点 1,恢复到 HP 500(在森林)GameMemento 的 getter 是 package-private(无 public 修饰符),外部类无法读取备忘录内部的 level、health 等字段,只有与 GameMemento 同包的 GameCharacter 才能访问。这是 Java 包私有访问控制在设计模式中的精妙运用。
第 4 章 访问者模式(Visitor)
4.1 动机:在不修改类的情况下添加新操作
访问者模式(Visitor Pattern)解决的问题是:当一个对象结构(通常是类层次结构)相对稳定,但经常需要为这些对象添加新操作时,如何在不修改这些类的前提下定义新操作。
这个模式最难理解,来看一个让它变得必要的场景——编译器的 AST(抽象语法树):
Expression(表达式,抽象基类)
├── NumberExpression(数字,如 42)
├── BinaryExpression(二元运算,如 a + b)
│ ├── left: Expression
│ └── right: Expression
└── VariableExpression(变量引用,如 x)
对这个 AST,需要多种操作:
- 求值(Evaluate):计算表达式的值;
- 代码生成(CodeGen):生成汇编代码;
- 类型检查(TypeCheck):检查类型是否兼容;
- 优化(Optimize):常量折叠等优化;
- 打印(Print):格式化输出表达式。
如果把每种操作都加到 Expression 类层次中(如在 Expression 中加 evaluate()、codeGen()、typeCheck() …),随着操作增多,类会变得越来越臃肿,且每次新增操作都要修改所有的 Expression 子类——对 AST 节点类型(节点少但操作多)来说,这违反了 OCP。
如果用 instanceof + switch:
// 反例:每次新增操作都要写一大堆 instanceof
public int evaluate(Expression expr) {
if (expr instanceof NumberExpression) {
return ((NumberExpression) expr).getValue();
} else if (expr instanceof BinaryExpression) {
BinaryExpression bin = (BinaryExpression) expr;
int left = evaluate(bin.getLeft());
int right = evaluate(bin.getRight());
return applyOp(bin.getOperator(), left, right);
} else if (expr instanceof VariableExpression) {
return env.get(((VariableExpression) expr).getName());
}
throw new UnsupportedOperationException();
}这是”单分派”(Single Dispatch)——evaluate() 的实现只根据一个类型(expr 的类型)决定行为。对于 AST 这种节点类型稳定但操作频繁变化的场景,更理想的是”双分派”(Double Dispatch)——操作由节点类型和访问者类型共同决定。
4.2 访问者模式的结构:双分派的实现
// 访问者接口:为每种 AST 节点定义一个 visit 方法
public interface ExpressionVisitor<T> {
T visitNumber(NumberExpression expr);
T visitBinary(BinaryExpression expr);
T visitVariable(VariableExpression expr);
}
// 抽象节点:接受访问者(关键的 accept 方法)
public abstract class Expression {
public abstract <T> T accept(ExpressionVisitor<T> visitor);
}
// 具体节点一:数字
public class NumberExpression extends Expression {
private final int value;
public NumberExpression(int value) { this.value = value; }
public int getValue() { return value; }
@Override
public <T> T accept(ExpressionVisitor<T> visitor) {
return visitor.visitNumber(this); // 双分派的关键:告诉访问者"我是 Number"
}
}
// 具体节点二:二元运算
public class BinaryExpression extends Expression {
private final Expression left;
private final String operator;
private final Expression right;
public BinaryExpression(Expression left, String op, Expression right) {
this.left = left; this.operator = op; this.right = right;
}
public Expression getLeft() { return left; }
public Expression getRight() { return right; }
public String getOperator() { return operator; }
@Override
public <T> T accept(ExpressionVisitor<T> visitor) {
return visitor.visitBinary(this); // 告诉访问者"我是 Binary"
}
}
// 具体访问者一:求值
public class EvaluateVisitor implements ExpressionVisitor<Integer> {
private final Map<String, Integer> environment;
@Override
public Integer visitNumber(NumberExpression expr) {
return expr.getValue();
}
@Override
public Integer visitBinary(BinaryExpression expr) {
int left = expr.getLeft().accept(this); // 递归访问子树
int right = expr.getRight().accept(this);
return switch (expr.getOperator()) {
case "+" -> left + right;
case "-" -> left - right;
case "*" -> left * right;
case "/" -> left / right;
default -> throw new UnsupportedOperationException("Unknown op: " + expr.getOperator());
};
}
@Override
public Integer visitVariable(VariableExpression expr) {
return environment.getOrDefault(expr.getName(), 0);
}
}
// 具体访问者二:打印(新增操作,不修改任何 Expression 类)
public class PrintVisitor implements ExpressionVisitor<String> {
@Override
public String visitNumber(NumberExpression expr) {
return String.valueOf(expr.getValue());
}
@Override
public String visitBinary(BinaryExpression expr) {
return "(" + expr.getLeft().accept(this) + " "
+ expr.getOperator() + " "
+ expr.getRight().accept(this) + ")";
}
@Override
public String visitVariable(VariableExpression expr) {
return expr.getName();
}
}
// 使用:(3 + 4) * 2
Expression ast = new BinaryExpression(
new BinaryExpression(new NumberExpression(3), "+", new NumberExpression(4)),
"*",
new NumberExpression(2)
);
System.out.println(ast.accept(new PrintVisitor())); // "(( 3 + 4 ) * 2 )"
System.out.println(ast.accept(new EvaluateVisitor(env))); // 14双分派的工作机制:
- 调用
ast.accept(visitor):第一次分派——根据ast的运行时类型(BinaryExpression)调用BinaryExpression.accept(); - 在
accept()中调用visitor.visitBinary(this):第二次分派——根据visitor的运行时类型(EvaluateVisitor)调用EvaluateVisitor.visitBinary()。
最终执行的方法由 Expression 的具体类型和 Visitor 的具体类型共同决定——这就是双分派。
4.3 访问者模式的固有权衡
访问者模式有一个固有的”双向性”权衡:
- 对新增操作友好(OCP 兼容):新增一个
TypeCheckVisitor或OptimizeVisitor,只需新增一个类,不修改任何Expression子类; - 对新增节点类型不友好(OCP 不兼容):如果 AST 新增
FunctionCallExpression节点,必须在所有现有Visitor接口和实现类中新增visitFunctionCall()方法——影响所有访问者。
这与抽象工厂的扩展困难形成了完美的对称:抽象工厂对新增产品族友好但对新增产品类型不友好;访问者对新增操作友好但对新增节点类型不友好。
选择原则:
- 类层次结构稳定(节点类型不会频繁变化)+ 操作频繁扩展 → 使用访问者;
- 类层次结构频繁变化(节点类型经常新增)+ 操作稳定 → 将操作放入类中(传统多态)。
4.4 Java 模式匹配对访问者模式的冲击
Java 16 引入了 instanceof 的模式匹配,Java 21 引入了 switch 的模式匹配,部分替代了传统访问者模式的使用场景:
// Java 21:switch 模式匹配,不需要访问者接口和 accept() 方法
public static int evaluate(Expression expr) {
return switch (expr) {
case NumberExpression n -> n.getValue();
case BinaryExpression b -> {
int left = evaluate(b.getLeft());
int right = evaluate(b.getRight());
yield switch (b.getOperator()) {
case "+" -> left + right;
case "*" -> left * right;
default -> throw new UnsupportedOperationException();
};
}
case VariableExpression v -> environment.getOrDefault(v.getName(), 0);
};
}这种写法不需要 accept() 方法,代码更简洁。但它仍然是单分派(只根据 expr 的类型分派),且”新增操作需要在多处写 switch”的问题仍然存在——不像访问者模式那样将所有操作集中在访问者类中。
对于需要多个独立操作、且每个操作逻辑较复杂的 AST 场景,传统访问者模式仍然更清晰;对于简单的单操作或操作逻辑简短的场景,Java 21 的模式匹配更简洁。
总结
本篇完成了行为型模式的最后四个:
-
状态模式将对象在不同状态下的行为封装到独立的状态类中,消灭了”用 if-else 管理状态跳转”的代码泥潭。状态迁移逻辑可以由状态类自己控制(内聚但有耦合),也可以由 Context 集中控制(解耦但 Context 复杂)。适用于状态数量多(3 个以上)且每种状态下的行为差异显著的对象;
-
中介者模式通过引入中间人,将多个对象之间的网状依赖简化为星型依赖,所有通信经过中介者协调。Spring MVC 的
DispatcherServlet是典型的工业级中介者。代价是中介者本身可能变得复杂——它集中了太多交互逻辑,要避免它演变成”上帝类”; -
备忘录模式通过保存对象的状态快照实现”历史回放”,同时通过 package-private 访问控制保护快照内部不被外部读写。与命令模式撤销的区别在于:命令存储操作,备忘录存储状态;命令适合有明确逆操作的场景,备忘录适合状态复杂、逆操作难以定义的场景(如游戏存档);
-
访问者模式通过双分派让”对节点类型的分派”和”对操作类型的分派”各自独立,实现在不修改节点类的情况下新增操作。固有局限是新增节点类型需要修改所有访问者接口,因此适合节点类型稳定、操作频繁扩展的场景(编译器 AST、文档对象模型)。Java 21 的 switch 模式匹配在简单场景下是更轻量的替代。
至此,23 种 GoF 设计模式的全部三大分类(5 种创建型、7 种结构型、11 种行为型)均已完成。下一篇从宏观视角出发,梳理设计模式在 Spring 框架源码中的综合应用全景:09 设计模式在Spring中的应用全景。
参考资料
- GoF,《Design Patterns: Elements of Reusable Object-Oriented Software》, 1994
- JEP 441: Pattern Matching for switch(Java 21)
- Spring Framework 源码:
DispatcherServlet(中介者)- Martin Fowler,《Refactoring》, Replace State-Altering Conditionals with State(状态模式的重构路径)
思考题
- Java 9 废弃了
java.util.Observable和Observer。Spring 的ApplicationEvent+@EventListener是更现代的观察者实现。同步事件发布(ApplicationEventPublisher.publishEvent())意味着所有监听器在发布线程中依次执行——如果某个监听器执行缓慢,会阻塞发布者。@Async+@EventListener能解决这个问题吗?异步事件在事务环境中有什么陷阱(如事务未提交时事件已被消费)?- 责任链模式在 Java Web 中随处可见——Servlet Filter、Spring Interceptor、Netty ChannelPipeline 都是责任链。Servlet Filter 的顺序由
web.xml或@Order注解决定。如果两个 Filter 的 Order 相同,它们的执行顺序是确定的吗?在微服务网关中,如何保证 Filter 链的顺序在多实例部署时一致?- 观察者模式中,如果观察者的
update()方法抛出异常,是否会影响其他观察者收到通知?Spring 的SimpleApplicationEventMulticaster默认行为是什么?在生产环境中,你如何保证即使某个监听器失败也不影响其他监听器?