行为型模式(下)——状态、中介者、备忘录与访问者

摘要

行为型模式的最后一批包含四个各具特色的模式。状态模式(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 类,修改 CancelledStatePaidState 中涉及退款的状态迁移逻辑——其他状态类完全不受影响。

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 中扮演的正是中介者角色:HandlerMappingHandlerAdapterViewResolverHandlerExceptionResolver 等组件不互相引用,而是都与 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

双分派的工作机制

  1. 调用 ast.accept(visitor):第一次分派——根据 ast 的运行时类型(BinaryExpression)调用 BinaryExpression.accept()
  2. accept() 中调用 visitor.visitBinary(this):第二次分派——根据 visitor 的运行时类型(EvaluateVisitor)调用 EvaluateVisitor.visitBinary()

最终执行的方法由 Expression 的具体类型和 Visitor 的具体类型共同决定——这就是双分派。

4.3 访问者模式的固有权衡

访问者模式有一个固有的”双向性”权衡:

  • 对新增操作友好(OCP 兼容):新增一个 TypeCheckVisitorOptimizeVisitor,只需新增一个类,不修改任何 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(状态模式的重构路径)

思考题

  1. Java 9 废弃了 java.util.ObservableObserver。Spring 的 ApplicationEvent + @EventListener 是更现代的观察者实现。同步事件发布(ApplicationEventPublisher.publishEvent())意味着所有监听器在发布线程中依次执行——如果某个监听器执行缓慢,会阻塞发布者。@Async + @EventListener 能解决这个问题吗?异步事件在事务环境中有什么陷阱(如事务未提交时事件已被消费)?
  2. 责任链模式在 Java Web 中随处可见——Servlet Filter、Spring Interceptor、Netty ChannelPipeline 都是责任链。Servlet Filter 的顺序由 web.xml@Order 注解决定。如果两个 Filter 的 Order 相同,它们的执行顺序是确定的吗?在微服务网关中,如何保证 Filter 链的顺序在多实例部署时一致?
  3. 观察者模式中,如果观察者的 update() 方法抛出异常,是否会影响其他观察者收到通知?Spring 的 SimpleApplicationEventMulticaster 默认行为是什么?在生产环境中,你如何保证即使某个监听器失败也不影响其他监听器?