行为型模式关注对象如何分配工作、传递请求和协调状态。同样使用接口与委托,观察者组织通知,策略选择算法,责任链安排处理顺序,命令保存操作,状态安排转换。需要先明确业务中的协作规则,再决定对象关系。

本章延续原有九种模式的范围。每个 Java 代码块可独立保存为 Demo.java,使用 JDK 17 或更高版本运行。

观察者模式(Observer Pattern) ⭐⭐⭐

一对多依赖与状态通知

观察者让一个发布者在状态变化时通知多个订阅者。发布者依赖统一的回调协议,新增订阅者可以参与通知,而无需把它们的具体类型写进发布者。

通知可以采用推送方式,把事件数据作为参数传给回调;也可以只通知变化,让订阅者再读取状态。不可变事件快照更容易表达“发生变化时的数据”,尤其适合之后再处理事件的场景。

先登记订阅,再按事件快照通知

图中的 event 是 PriceChanged(100, 90)。发布者先提交自己的状态,再同步通知订阅快照。某个回调失败不会撤销价格变化,示例的后续通知会随异常终止。

Java
// 观察者:发布者保存订阅,变化后向回调推送事件快照。
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
 
public class Demo {
    // 不可变事件保存变化时的新旧价格,回调不用再读取可变状态。
    record PriceChanged(int oldPrice, int newPrice) {}
 
    // 观察者协议:发布者只知道这个回调接口。
    interface PriceListener {
        void onPriceChanged(PriceChanged event);
    }
 
    static final class PriceSubject {
        private final List<PriceListener> listeners = new ArrayList<>();
        private int price;
 
        PriceSubject(int price) {
            if (price < 0) {
                throw new IllegalArgumentException("价格不能为负");
            }
            this.price = price;
        }
 
        // 返回取消句柄,让订阅拥有者能明确解除注册。
        AutoCloseable subscribe(PriceListener listener) {
            Objects.requireNonNull(listener);
            if (listeners.contains(listener)) {
                throw new IllegalArgumentException("监听器已注册");
            }
            listeners.add(listener);
            // 关闭句柄就是取消订阅,避免发布者长期持有回调。
            return () -> listeners.remove(listener);
        }
 
        void setPrice(int newPrice) {
            if (newPrice < 0) {
                throw new IllegalArgumentException("价格不能为负");
            }
            if (price == newPrice) {
                return;
            }
            // 先记录本次变化的快照,再更新发布者状态。
            PriceChanged event = new PriceChanged(price, newPrice);
            price = newPrice;
            // 遍历订阅快照,回调期间取消订阅不破坏当前列表遍历。
            for (PriceListener listener : List.copyOf(listeners)) {
                listener.onPriceChanged(event);
            }
        }
    }
 
    public static void main(String[] args) throws Exception {
        PriceSubject subject = new PriceSubject(100);
        PriceListener logger = event -> System.out.println("记录变化:" + event);
        PriceListener screen = event -> System.out.println("显示新价格:" + event.newPrice());
        // 退出作用域时自动关闭两个订阅。
        try (AutoCloseable logSubscription = subject.subscribe(logger);
             AutoCloseable screenSubscription = subject.subscribe(screen)) {
            subject.setPrice(90); // 两个监听器都会收到通知
        }
        subject.setPrice(80);     // 已取消订阅,不再通知
    }
}

角色分别是 PriceSubject、PriceListener 和两个具体回调。订阅方法返回取消句柄,让拥有订阅的调用者明确负责解除注册。

通知时的边界

示例按注册顺序同步调用回调,并复制监听器列表后遍历。某个监听器在回调中取消订阅,不会导致当前遍历的集合修改异常;它仍可能已经属于本轮通知快照。

如果一个回调抛出异常,示例会立即终止本轮通知,价格已经修改,后续回调不会执行。业务若要求尽量通知所有监听器,可以逐个捕获并记录异常,但应明确失败汇报和重试协议,避免默默吞掉错误。

回调线程、通知顺序与异常策略都属于观察者协议。 上面的实现用于单线程场景。跨线程发布还需要线程安全的订阅管理、状态同步和事件顺序保证。监听器在回调中再次改变价格,也会产生嵌套通知;需要严格串行顺序时可以用事件队列。

发布者持有监听器引用,监听器可能捕获其他对象。长生命周期发布者若一直保留短生命周期监听器,会延长这些对象的存活时间。使用完成后取消订阅,比只讨论弱引用更直接。

JDK 应用:属性变化与流式订阅

PropertyChangeSupport 提供属性变化监听能力,可在普通 Java 中使用。

Java
// 属性监听:对象修改属性后,通过 JDK 通知协议发出变化。
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
 
public class Demo {
    static final class Profile {
        // 每个 Profile 持有自己的监听器管理器。
        private final PropertyChangeSupport changes = new PropertyChangeSupport(this);
        private String name = "未命名";
 
        void addListener(PropertyChangeListener listener) {
            changes.addPropertyChangeListener(listener);
        }
 
        void removeListener(PropertyChangeListener listener) {
            changes.removePropertyChangeListener(listener);
        }
 
        void setName(String newName) {
            // 先保存旧值,事件才能同时说明变化前后的内容。
            String oldName = name;
            name = newName;
            // 修改状态后发出属性事件;回调在当前调用线程执行。
            changes.firePropertyChange("name", oldName, newName);
        }
    }
 
    public static void main(String[] args) {
        Profile profile = new Profile();
        PropertyChangeListener listener = event -> System.out.println(
                event.getPropertyName() + ":" + event.getOldValue() + " -> " + event.getNewValue());
        // 注册的对象需要保留,后续用同一个引用解除注册。
        profile.addListener(listener);
        profile.setName("小林");
        // 使用结束解除监听,避免不必要的长期引用。
        profile.removeListener(listener);
    }
}

旧的 java.util.Observable 和 Observer 自 Java 9 起已弃用,新设计可以使用自定义监听器或合适的事件 API。Flow.Publisher、Flow.Subscriber、Flow.Subscription 进一步定义订阅、请求数量和取消协议:订阅者通过 request(n) 表达需求,这是响应式流中的背压语义,不能只当成普通广播理解。

策略模式(Strategy Pattern) ⭐⭐

封装可替换的算法

策略把某个算法表达为统一协议,让使用者在不同实现之间选择。上下文负责调用算法,具体策略负责计算规则。

上下文把算法交给选定策略

上下文负责共同输入校验,策略负责具体运费计算。运行时切换改变的是接口引用指向的对象。

Java
// 策略:上下文负责调用,具体策略维护运费算法。
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Objects;
 
public class Demo {
    // 算法协议,重量单位统一为克。
    interface ShippingStrategy {
        BigDecimal cost(int weightGrams);
    }
 
    // 普通配送策略单独维护重量分段规则。
    static final class StandardShipping implements ShippingStrategy {
        @Override
        public BigDecimal cost(int weightGrams) {
            return weightGrams <= 1000 ? new BigDecimal("8.00") : new BigDecimal("12.00");
        }
    }
 
    static final class ExpressShipping implements ShippingStrategy {
        @Override
        public BigDecimal cost(int weightGrams) {
            return new BigDecimal("15.00")
                    .add(BigDecimal.valueOf(weightGrams).movePointLeft(3))
                    // 金额结果按两位小数舍入,规则在策略中明确。
                    .setScale(2, RoundingMode.HALF_UP);
        }
    }
 
    static final class ShippingCalculator {
        // 上下文只持有接口引用,可以替换实际算法。
        private ShippingStrategy strategy;
 
        ShippingCalculator(ShippingStrategy strategy) {
            setStrategy(strategy);
        }
 
        void setStrategy(ShippingStrategy strategy) {
            this.strategy = Objects.requireNonNull(strategy);
        }
 
        BigDecimal calculate(int weightGrams) {
            if (weightGrams <= 0) {
                throw new IllegalArgumentException("重量必须为正");
            }
            // 业务校验后委托算法,无需按策略类型编写分支。
            return strategy.cost(weightGrams);
        }
    }
 
    public static void main(String[] args) {
        ShippingCalculator calculator = new ShippingCalculator(new StandardShipping());
        System.out.println(calculator.calculate(500)); // 8.00
        // 替换引用后,后续计算使用新的算法。
        calculator.setStrategy(new ExpressShipping());
        System.out.println(calculator.calculate(500)); // 15.50
        // 单方法策略也可用 Lambda 表达。
        calculator.setStrategy(weight -> BigDecimal.ZERO);
        System.out.println(calculator.calculate(500)); // 0
    }
}

运行时切换来自上下文的策略引用变化,算法实现本身彼此独立。最后一项使用 Lambda,实现单抽象方法接口,表达免运费规则。

示例上下文可变,适合单线程演示。多个请求并发使用时,可以每个请求传入策略,或使用不可变上下文;不要把一个请求的策略选择存进所有请求共享的可变对象。

这个接口把重量单位定为克,把金额精度处理留给策略。若系统需要所有算法保证两位小数,可以把这一保证写进接口契约,并对每个策略按同一组规则验证。Lambda 是实现协议的一种语法形式,适用的输入与输出约束仍然相同。

注册与选择仍然需要规则

策略可以把复杂算法从长分支中抽离,但系统仍需要决定使用哪种策略。选择可以放在工厂、配置或注册表中。应区分“选择策略”和“执行策略”,并明确未知类型的处理方式。

当算法只有一两个简短分支且很稳定时,普通条件语句可能更清楚。适合提取策略的迹象是算法独立变化、需要单独测试,或调用者需要自行提供算法。

JDK 应用:Comparator 与函数式接口

Comparator 把排序规则交给调用者。函数式接口还可以组合规则,无需为每一种简单算法声明一个类。

Java
// Comparator:将排序算法所需的比较规则交给调用者。
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
 
public class Demo {
    record Product(String name, int price) {}
 
    public static void main(String[] args) {
        List<Product> products = new ArrayList<>(List.of(
                new Product("Book", 60),
                new Product("Pen", 10),
                new Product("Notebook", 10)));
 
        // 先比较价格,相同价格再比较名称。
        Comparator<Product> byPrice = Comparator.comparingInt(Product::price)
                // 组合比较规则,保持同价商品的确定顺序。
                .thenComparing(Product::name);
        // 排序操作通过比较器协议执行。
        products.sort(byPrice);
        System.out.println(products);
 
        // 换一套比较规则,集合与排序入口仍然相同。
        products.sort(Comparator.comparing(Product::name));
        System.out.println(products);
    }
}

比较器需要满足比较协议,例如反对称性、传递性和相等分组的一致性。随意依赖可变化的外部状态可能破坏这些要求。方法引用和 Lambda 减少了语法负担,算法契约仍然需要保证。

模板方法模式(Template Method Pattern) ⭐

固定算法骨架,开放特定步骤

模板方法在父类中规定流程,在子类中实现部分步骤。稳定流程通常声明为 final,让子类扩展已经设计好的钩子。

固定流程与子类扩展点

子类改变解析步骤,父类保留步骤顺序。成功后的钩子与始终需要执行的清理要分开,资源释放仍由 finally 或 try-with-resources 保证。

Java
// 模板方法:父类约定步骤顺序,子类只实现开放的步骤。
import java.util.List;
 
public class Demo {
    abstract static class DataImporter {
        // 固定算法骨架,子类不能改写整个导入顺序。
        final void importData(String source) {
            beforeImport();
            // 多态调用子类提供的解析步骤。
            List<String> rows = parse(source);
            // 解析成功后校验;失败就不执行后续保存。
            validate(rows);
            save(rows);
            // 仅成功路径调用,不能用它代替 finally 中的清理。
            afterImport(rows.size());
        }
 
        // 可选钩子默认不执行动作,需要时再重写。
        protected void beforeImport() {}
 
        // 必需扩展点,每个具体导入器都要提供实现。
        protected abstract List<String> parse(String source);
 
        protected void validate(List<String> rows) {
            if (rows.isEmpty() || rows.stream().anyMatch(String::isBlank)) {
                throw new IllegalArgumentException("导入数据不能为空");
            }
        }
 
        protected void save(List<String> rows) {
            System.out.println("保存:" + rows);
        }
 
        protected void afterImport(int count) {
            System.out.println("导入 " + count + " 行");
        }
    }
 
    static final class LineImporter extends DataImporter {
        @Override
        protected List<String> parse(String source) {
            return source.lines().map(String::strip).toList();
        }
    }
 
    static final class SemicolonImporter extends DataImporter {
        @Override
        protected List<String> parse(String source) {
            return List.of(source.split(";", -1));
        }
    }
 
    public static void main(String[] args) {
        new LineImporter().importData("Java\n设计模式");
        new SemicolonImporter().importData("Java;设计模式");
    }
}

importData() 固定解析、校验、保存和报告顺序,两个子类只改变解析方式。具有默认实现的钩子可以按需重写,必须实现的步骤用抽象方法表达。

继承协议与失败路径

父类应说明钩子何时调用、允许修改什么状态,以及失败后会执行哪些步骤。示例中解析或保存失败就停止,afterImport() 只在成功后调用。如果某个钩子需要释放资源,应使用 finally 或 try-with-resources,不能把成功钩子当成清理保证。

避免在父类构造函数中调用可重写方法。子类字段在父类构造时可能尚未初始化,动态分派会让子类方法看到不完整状态。

JDK 应用与策略的取舍

AbstractList、AbstractCollection 等骨架实现复用稳定操作,并让子类实现必要的基础能力。阅读这类 API 时,需要明确哪些方法必须实现、哪些默认方法依赖它们,以及修改操作是否受支持。

模板方法使用继承确定步骤,策略使用组合替换算法。若多个步骤都需要独立切换,可以把它们变成协作者;若流程稳定且子类型自然存在,模板方法可以清楚地约束扩展方式。

责任链模式(Chain of Responsibility Pattern) ⭐⭐

沿链传递请求

责任链把多个处理者组织为一个可配置的处理序列。每个处理者决定处理、拒绝、停止或继续传递,发送者只需要进入链。

一种常见形式是“找到处理者后停止”,例如按级别审批;另一种是多个处理者协作完成请求,例如日志、鉴权、校验和业务处理。应在链的协议中说明是哪一种。

拦截器式链与返回路径

下面用索引构建处理链。处理器调用 proceed() 进入后续处理,可以在调用前后执行行为,也可以直接返回以短路后续流程。

成功请求与两种短路的时序

401 路径从鉴权直接返回给外层日志,400 路径从校验返回,两种情况都不会执行最终业务。成功路径逐层进入,再按调用栈相反顺序返回。

Java
// 责任链:处理者可以继续或短路,结果按调用栈逐层返回。
import java.util.List;
import java.util.Objects;
 
public class Demo {
    record Request(String user, String payload) {}
    record Response(int status, String body) {}
 
    // 处理者通过 proceed 请求继续进入后续链。
    interface Chain {
        Response proceed(Request request);
    }
 
    interface Interceptor {
        Response intercept(Request request, Chain chain);
    }
 
    interface Handler {
        Response handle(Request request);
    }
 
    static final class RealChain implements Chain {
        private final List<Interceptor> interceptors;
        private final int index;
        private final Handler terminal;
        // 每次执行创建独立节点,限制当前节点最多继续一次。
        private boolean called;
 
        RealChain(List<Interceptor> interceptors, int index, Handler terminal) {
            this.interceptors = interceptors;
            this.index = index;
            this.terminal = terminal;
        }
 
        @Override
        public Response proceed(Request request) {
            // 拒绝重复执行后续链,避免业务处理被意外调用两次。
            if (called) {
                throw new IllegalStateException("每个链节点最多继续一次");
            }
            called = true;
            // 没有后续拦截器时才进入最终业务处理。
            if (index == interceptors.size()) {
                return terminal.handle(request);
            }
            Interceptor interceptor = interceptors.get(index);
            return interceptor.intercept(request,
                    // 每次把索引向后推进,表达请求传递顺序。
                    new RealChain(interceptors, index + 1, terminal));
        }
    }
 
    static final class Pipeline {
        private final List<Interceptor> interceptors;
        private final Handler terminal;
 
        Pipeline(List<Interceptor> interceptors, Handler terminal) {
            this.interceptors = List.copyOf(interceptors);
            this.terminal = Objects.requireNonNull(terminal);
        }
 
        Response execute(Request request) {
            // 每个请求从新链开始,不共享本次执行的 called 状态。
            return new RealChain(interceptors, 0, terminal).proceed(request);
        }
    }
 
    public static void main(String[] args) {
        Interceptor logging = (request, chain) -> {
            System.out.println("日志:开始");
            // 进入后续链;返回后外层日志继续观察处理结果。
            Response response = chain.proceed(request);
            System.out.println("日志:结束 " + response.status());
            return response;
        };
        Interceptor authentication = (request, chain) -> {
            if (request.user() == null || request.user().isBlank()) {
                // 短路:直接返回,校验和最终业务处理都不会执行。
                return new Response(401, "需要登录");
            }
            return chain.proceed(request);
        };
        Interceptor validation = (request, chain) -> {
            if (request.payload() == null || request.payload().isBlank()) {
                return new Response(400, "内容不能为空");
            }
            return chain.proceed(request);
        };
 
        Pipeline pipeline = new Pipeline(List.of(logging, authentication, validation),
                request -> new Response(200, "保存:" + request.payload()));
        System.out.println(pipeline.execute(new Request("小林", "订单")));
        System.out.println(pipeline.execute(new Request("", "订单")));
    }
}

正常请求按日志、鉴权、校验、终点的顺序进入,返回时逐层回到调用者。无用户请求在鉴权处返回 401,校验与终点不会执行,外层日志仍能看到返回值。

链的协议要明确

本示例允许短路,每个节点最多调用一次 proceed()。某些网络客户端的重试逻辑可能需要多次执行后续链,这属于另一种协议,不能直接套用这里的限制。

链每次执行创建新的节点,避免多个请求共享 called 状态。处理器对象仍然可能被复用,因此它们的字段也要遵循相应并发约束。

链顺序会影响行为:鉴权之前记录完整请求可能涉及敏感数据,校验之后才记日志可能失去错误请求信息。错误抛出时,返回后的代码不会执行;需要计时或清理时用 finally。

Servlet Filter、日志过滤器和 HTTP 客户端拦截器都是理解责任链的常见场景。只把几个方法按顺序写在一个函数中,还没有体现处理者独立与传递协议带来的配置能力。

命令模式(Command Pattern)

把请求表达为对象

命令将操作及其执行信息封装成对象,让发送者与真正执行工作的接收者解耦。命令对象还可以进入队列、记录历史、组成批次或交给调度器。

Runnable 可以表达无返回值的操作,Callable 可以表达带返回值、允许受检异常的操作。它们能承担轻量命令协议,但并没有自动提供撤销、重试或持久化语义。

执行、撤销与重做

下面的文本编辑器通过追加命令保存操作前的状态。历史管理器只在执行成功后记录命令,撤销和重做也在成功后移动历史项。

执行、撤销与重做如何移动历史记录

历史记录在操作成功后才移动。撤销后执行新命令意味着产生新分支,原来的重做路线不再有效。

Java
// 命令:操作对象维护执行信息,历史对象维护撤销与重做顺序。
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;
 
public class Demo {
    static final class Editor {
        private String text = "";
 
        String text() {
            return text;
        }
 
        void replace(String text) {
            this.text = Objects.requireNonNull(text);
        }
    }
 
    // 操作协议,execute 与 undo 的含义由具体命令兑现。
    interface Command {
        void execute();
        void undo();
    }
 
    static final class AppendCommand implements Command {
        private final Editor editor;
        private final String addition;
        // 命令保存执行前状态,供之后的撤销使用。
        private String before;
        private boolean applied;
 
        AppendCommand(Editor editor, String addition) {
            this.editor = Objects.requireNonNull(editor);
            this.addition = Objects.requireNonNull(addition);
        }
 
        @Override
        public void execute() {
            if (applied) {
                throw new IllegalStateException("命令已经执行");
            }
            // 执行时读取当下状态,而不是构造命令时提前记录。
            before = editor.text();
            // 命令把具体工作交给接收者 Editor。
            editor.replace(before + addition);
            applied = true;
        }
 
        @Override
        public void undo() {
            if (!applied) {
                throw new IllegalStateException("命令尚未执行");
            }
            // 撤销恢复本命令执行前的文本。
            editor.replace(before);
            applied = false;
        }
    }
 
    static final class History {
        private final Deque<Command> undoStack = new ArrayDeque<>();
        private final Deque<Command> redoStack = new ArrayDeque<>();
 
        void execute(Command command) {
            if (undoStack.contains(command) || redoStack.contains(command)) {
                throw new IllegalArgumentException("每次新操作使用新的命令对象");
            }
            // 新操作先执行,成功后才写入撤销历史。
            command.execute();
            undoStack.push(command);
            // 撤销后的新操作创建新分支,旧重做历史失效。
            redoStack.clear();
        }
 
        void undo() {
            if (undoStack.isEmpty()) {
                return;
            }
            // 先查看栈顶,撤销成功后再移走记录。
            Command command = undoStack.peek();
            command.undo();
            undoStack.pop();
            redoStack.push(command);
        }
 
        void redo() {
            if (redoStack.isEmpty()) {
                return;
            }
            // 重做同样先执行,成功后再把记录放回撤销栈。
            Command command = redoStack.peek();
            command.execute();
            redoStack.pop();
            undoStack.push(command);
        }
    }
 
    public static void main(String[] args) {
        Editor editor = new Editor();
        History history = new History();
        history.execute(new AppendCommand(editor, "Java"));
        history.execute(new AppendCommand(editor, " 设计模式"));
        System.out.println(editor.text()); // Java 设计模式
        history.undo();
        System.out.println(editor.text()); // Java
        history.redo();
        System.out.println(editor.text()); // Java 设计模式
        history.undo();
        history.execute(new AppendCommand(editor, " 学习笔记"));
        history.redo(); // 新分支已经清空旧的重做历史
        System.out.println(editor.text()); // Java 学习笔记
    }
}

编辑器是接收者,AppendCommand 是具体命令,History 是调用者并管理操作历史。撤销后的新操作会形成新历史分支,旧的重做记录需要清空。

这个协议假定所有修改都经过历史管理器,并按栈顺序撤销。如果其他代码直接修改编辑器,保存的旧状态可能覆盖后来的修改。协作编辑或外部资源操作还需要版本号和冲突处理。

宏命令与补偿

宏命令可以按顺序执行多个子命令,撤销时按相反顺序处理。中途失败时是否回滚已经完成的子命令,以及回滚本身失败后如何汇报,都需要额外协议。

发送邮件、外部扣款等操作通常无法通过恢复本地字段撤销。它们可能需要补偿操作、幂等键或业务事务,不能因为实现了 undo() 就认为外部世界已恢复原状。

JDK 应用:线程池执行命令

提交与执行分开,结果由 Future 观察

任务对象描述要做什么,执行器负责执行安排。任务异常通过 Future 汇报,调用 get 才会等待这次执行结束。

Java
// 线程池命令:提交操作对象,执行器决定何时在哪个线程运行。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
public class Demo {
    public static void main(String[] args) throws Exception {
        // 单线程执行器按队列组织任务,这里不自行创建工作线程。
        ExecutorService executor = Executors.newSingleThreadExecutor();
        try {
            // Runnable 表达无返回值任务,Future 仍能观察完成与异常。
            Future<?> notification = executor.submit(
                    () -> System.out.println("执行通知任务"));
            // 等待完成;执行失败会通过 Future 汇报给调用者。
            notification.get();
 
            // Callable 表达有返回值的任务。
            Future<Integer> calculation = executor.submit(() -> 6 * 7);
            System.out.println(calculation.get()); // 42
        } finally {
            // 最后关闭执行器,避免线程池生命周期失控。
            executor.shutdown();
        }
    }
}

线程池把操作与执行线程分离。Future.get() 可以观察结果和执行异常;提交成功只说明任务已交给执行器。任务所捕获的对象如果会继续变化,还要明确它应读取提交时的快照还是执行时的状态。

状态模式(State Pattern)

当前状态决定行为

状态模式让上下文把操作交给当前状态对象,状态对象定义允许的行为与下一状态。适合一个对象在不同阶段对同一操作有不同响应,且转换规则需要集中检查的情况。

下面的订单有待支付、已支付、已发货、已取消四种状态。

订单允许的状态转换

图中只画允许的转换。其他操作在示例中抛异常;已支付后的退款属于另一个需要明确设计的流程。终止标记表示示例流程结束,不表示删除订单对象。

Java
// 状态:对象把操作交给当前状态,状态决定有效行为和转换。
public class Demo {
    // 同一操作在不同状态下有不同规则。
    interface OrderState {
        String name();
 
        // 默认拒绝;具体状态只开放自己允许的操作。
        default void pay(Order order) {
            throw new IllegalStateException(name() + " 状态不能支付");
        }
 
        default void ship(Order order) {
            throw new IllegalStateException(name() + " 状态不能发货");
        }
 
        default void cancel(Order order) {
            throw new IllegalStateException(name() + " 状态不能取消");
        }
    }
 
    static final class Pending implements OrderState {
        @Override
        public String name() {
            return "待支付";
        }
 
        @Override
        public void pay(Order order) {
            // 待支付状态允许支付,成功后进入已支付。
            order.transition(new Paid());
        }
 
        @Override
        public void cancel(Order order) {
            order.transition(new Cancelled());
        }
    }
 
    static final class Paid implements OrderState {
        @Override
        public String name() {
            return "已支付";
        }
 
        @Override
        public void ship(Order order) {
            // 已支付状态允许发货,之后由新状态决定行为。
            order.transition(new Shipped());
        }
    }
 
    static final class Shipped implements OrderState {
        @Override
        public String name() {
            return "已发货";
        }
    }
 
    static final class Cancelled implements OrderState {
        @Override
        public String name() {
            return "已取消";
        }
    }
 
    static final class Order {
        // 上下文维护当前状态,初始为待支付。
        private OrderState state = new Pending();
 
        private void transition(OrderState next) {
            System.out.println(state.name() + " -> " + next.name());
            state = next;
        }
 
        void pay() {
            // 上下文把操作委托给当前状态,并传入自身供状态转换。
            state.pay(this);
        }
 
        void ship() {
            state.ship(this);
        }
 
        void cancel() {
            state.cancel(this);
        }
 
        String status() {
            return state.name();
        }
    }
 
    public static void main(String[] args) {
        Order first = new Order();
        first.pay();
        first.ship();
        System.out.println(first.status());
        try {
            // 已发货状态未开放取消,调用会明确失败。
            first.cancel();
        } catch (IllegalStateException e) {
            System.out.println(e.getMessage());
        }
 
        Order second = new Order();
        second.cancel();
        System.out.println(second.status());
    }
}

共同协议明确允许拒绝非法操作;具体状态只开放有效转换。已支付订单的取消没有在示例中开放,因为退款需要单独的业务流程。状态图、表格和代码应表达同一组转换。

当前状态支付发货取消
待支付已支付拒绝已取消
已支付拒绝已发货拒绝,需另建退款流程
已发货拒绝拒绝拒绝
已取消拒绝拒绝拒绝

转换由谁驱动

转换可以由状态对象决定,也可以由上下文集中管理转换表。状态对象负责局部行为通常更自然,集中表更便于审查全部规则;复杂流程可能同时采用两者。

并发支付与取消请求还需要让检查和转换成为受控的原子操作。持久化订单可能通过事务、版本号或条件更新保证,状态类本身不会自动解决并发竞争。

状态与策略

策略一般由调用者根据需求选择算法;状态跟随对象生命周期变化,转换会改变之后允许的操作。二者都可以通过接口委托,但选择来源和状态转换语义不同。

对比策略状态
谁决定当前实现调用者或配置选择算法当前阶段与转换规则决定
替换意味着什么换一种计算方式对象进入另一业务阶段
实现之间是否知道转换通常各策略互相独立状态可能决定下一状态
本章示例普通与加急运费待支付、已支付、已发货

当状态数量少、每个分支很短时,用 enum 和明确的 switch 可以更直接。只有状态相关行为不断增加、各阶段规则复杂时,拆成状态对象才更容易维护。

迭代器模式(Iterator Pattern)

遍历协议与内部结构分离

迭代器让调用者按统一协议逐个取得元素,无需知道集合内部是数组、链表还是树。Java 中 Iterable<T> 提供迭代器,Iterator<T> 提供 hasNext() 与 next() 等方法。

游标推进与结构修改检测

hasNext 只询问是否还有元素,next 负责取得元素并推进游标。修改检测用于发现错误,跨线程访问仍需要其他同步协议。

Java
// 迭代器:每次遍历拥有独立游标,集合内部表示对调用者隐藏。
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Objects;
 
public class Demo {
    // 聚合对象提供 iterator,增强 for 循环因此能遍历它。
    static final class Shelf implements Iterable<String> {
        private final String[] books;
        private int size;
        private int modCount;
 
        Shelf(int capacity) {
            if (capacity < 0) {
                throw new IllegalArgumentException("容量不能为负");
            }
            books = new String[capacity];
        }
 
        void add(String book) {
            Objects.requireNonNull(book);
            if (size == books.length) {
                throw new IllegalStateException("书架已满");
            }
            books[size++] = book;
            // 结构修改时增加计数,供迭代器检测非法修改。
            modCount++;
        }
 
        @Override
        public Iterator<String> iterator() {
            // 每次调用创建新迭代器,多个遍历不会共享游标。
            return new Iterator<>() {
                private int cursor;
                // 记录创建迭代器时的结构版本。
                private final int expectedModCount = modCount;
 
                private void checkModification() {
                    // 版本不一致时尽早报错;这不是并发互斥。
                    if (modCount != expectedModCount) {
                        throw new ConcurrentModificationException();
                    }
                }
 
                @Override
                public boolean hasNext() {
                    checkModification();
                    return cursor < size;
                }
 
                @Override
                public String next() {
                    checkModification();
                    if (cursor >= size) {
                        // 遍历结束按 Iterator 契约抛异常,不能用 null 充当终点。
                        throw new NoSuchElementException();
                    }
                    // 取出当前元素后推进这个迭代器自己的游标。
                    return books[cursor++];
                }
            };
        }
    }
 
    public static void main(String[] args) {
        Shelf shelf = new Shelf(3);
        shelf.add("Java");
        shelf.add("设计模式");
        for (String book : shelf) {
            System.out.println(book);
        }
 
        Iterator<String> iterator = shelf.iterator();
        shelf.add("集合");
        try {
            iterator.next();
        } catch (ConcurrentModificationException e) {
            System.out.println("检测到结构修改");
        }
    }
}

每次调用 iterator() 创建独立游标,多个遍历可以各自推进。遍历结束后 next() 必须抛出 NoSuchElementException,不能用 null 表示结束,因为某些集合允许 null 元素。

默认 Iterator.remove() 可以抛出 UnsupportedOperationException,本示例沿用这个行为。如果提供移除能力,还要更新游标和预期修改计数,避免跳过元素或错误地检测到自己的修改。

Fail-fast 的实际含义

ArrayList 等集合的迭代器通过修改计数检测部分非法结构修改,尽早暴露错误。Fail-fast 是错误检测机制,不是并发同步机制,也不保证发现所有竞争。

示例会在 hasNext() 和 next() 都检查修改,不代表每个 JDK 迭代器在这两个方法中都采用相同检查。调用者应依赖文档中的遍历契约。

需要一边遍历一边删除时,可使用支持删除的迭代器,或用 removeIf() 表达集合自身的删除规则。

Java
// 遍历中删除:使用集合提供的删除协议维护游标与结构版本。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
 
public class Demo {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4));
        // 显式迭代器让调用者控制遍历和删除。
        Iterator<Integer> iterator = numbers.iterator();
        while (iterator.hasNext()) {
            // 先取得元素,再判断是否移除。
            if (iterator.next() % 2 == 0) {
                // 删除刚返回的元素,由迭代器更新自身状态。
                iterator.remove();
            }
        }
        System.out.println(numbers); // [1, 3]
        // 也可以交给集合按谓词删除,避免自行控制游标。
        numbers.removeIf(value -> value < 3);
        System.out.println(numbers); // [3]
    }
}

外部、内部与并发遍历

显式 Iterator 由调用者控制推进,属于外部迭代。forEach() 等由集合控制遍历、调用者提供行为,可以理解为内部迭代。Stream 进一步组合延迟执行的处理步骤,需要考虑操作的无干扰要求。

CopyOnWriteArrayList 的迭代器读取创建时的数组快照,适合读多写少的情况。ConcurrentHashMap 的遍历是弱一致的,不等于固定快照。选择集合时应根据需要的观察语义和修改成本决定。

备忘录模式(Memento Pattern)

保存并恢复状态

备忘录让对象在保留内部封装的情况下保存快照,之后再恢复。原发器了解状态,备忘录保存状态,管理者持有快照但无需了解内部表示。

拥有者创建并解释快照

历史只保存并交回恢复凭据。编辑器负责解释快照,也负责拒绝其他编辑器的快照,封装边界保持在原发器内部。

Java
// 备忘录:编辑器保存快照,历史只持有不透明的恢复凭据。
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;
 
public class Demo {
    // 管理者看到的窄接口,不开放内部文本和光标字段。
    interface Memento {}
 
    static final class Editor {
        private String text = "";
        private int cursor;
 
        // 真正快照由 Editor 定义,字符串和整数可安全按值保存。
        private record Snapshot(Editor owner, String text, int cursor) implements Memento {}
 
        void replace(String text, int cursor) {
            Objects.requireNonNull(text);
            if (cursor < 0 || cursor > text.length()) {
                throw new IllegalArgumentException("光标越界");
            }
            this.text = text;
            this.cursor = cursor;
        }
 
        Memento save() {
            // 保存拥有者身份,避免把快照用于其他编辑器。
            return new Snapshot(this, text, cursor);
        }
 
        void restore(Memento memento) {
            // 恢复前同时验证快照类型与所属对象。
            if (!(memento instanceof Snapshot snapshot) || snapshot.owner() != this) {
                throw new IllegalArgumentException("快照不属于当前编辑器");
            }
            // 只有原发器负责解释快照并恢复自身状态。
            text = snapshot.text();
            cursor = snapshot.cursor();
        }
 
        String display() {
            return text + ",光标 " + cursor;
        }
    }
 
    static final class History {
        private final Deque<Memento> snapshots = new ArrayDeque<>();
 
        void checkpoint(Editor editor) {
            snapshots.push(editor.save());
        }
 
        void undo(Editor editor) {
            if (snapshots.isEmpty()) {
                return;
            }
            // 管理者交回快照;恢复成功后才移除历史记录。
            editor.restore(snapshots.peek());
            snapshots.pop();
        }
    }
 
    public static void main(String[] args) {
        Editor editor = new Editor();
        History history = new History();
        editor.replace("Java", 4);
        history.checkpoint(editor);
        editor.replace("Java 设计模式", 9);
        System.out.println(editor.display());
        history.undo(editor);
        System.out.println(editor.display()); // Java,光标 4
 
        try {
            new Editor().restore(editor.save());
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}

管理者只看到 Memento,快照表示由 Editor 定义。拥有者身份检查避免把一个编辑器的快照误用于另一个编辑器。独立文件中的私有快照类型和模块边界可以进一步限制访问。

快照与深拷贝

这里的状态由字符串和整数组成,快照可以安全保存值。若编辑器状态包含可变段落列表,仅用 record 包裹列表引用会让快照随原对象一起变化,需要复制列表及必要的可变元素。

快照的内存成本可能与文档大小和历史长度成正比。大型对象可以使用增量记录、持久化数据结构或限制历史数量。持有拥有者引用也会延长编辑器生命周期,历史管理器的生命周期应与编辑器相协调。

备忘录、命令与持久化

备忘录保存状态,命令保存操作。两者可以组合:命令执行前取得备忘录,撤销时交回原发器。只保存完整快照比较直接,但可能占更多内存;保存可逆操作可能更小,却需要更严格的操作协议。

跨进程恢复还需要版本化格式、数据校验与资源重建。进程内快照对象不能直接代替磁盘存档协议,文件句柄和数据库连接也通常无法按原身份恢复。

中介者模式(Mediator Pattern)

集中参与者之间的交互规则

多个对象彼此直接引用时,一项交互规则可能散落在多个参与者中。中介者让参与者通过统一协调对象交互,各自专注于自身行为。

聊天室中,用户通过房间发送消息,房间决定转给谁。用户无需持有其他用户列表。

成员通过房间交互

用户只持有房间协议,不持有其他用户列表。图表示可参与的交互方向;每轮转发会排除发送者,已离开的成员也不能继续发送。

Java
// 中介者:房间管理成员和转发规则,用户不保存其他用户的引用。
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
 
public class Demo {
    // 参与者通过协调协议发送,避免直接互相调用。
    interface ChatMediator {
        void send(User sender, String message);
    }
 
    static final class User {
        private final String name;
        private final ChatMediator mediator;
 
        User(String name, ChatMediator mediator) {
            this.name = Objects.requireNonNull(name);
            this.mediator = Objects.requireNonNull(mediator);
        }
 
        void send(String message) {
            // 用户把自身与消息交给房间,由房间选择接收者。
            mediator.send(this, message);
        }
 
        void receive(String from, String message) {
            System.out.println(name + " 收到 " + from + ":" + message);
        }
    }
 
    static final class ChatRoom implements ChatMediator {
        // 成员关系集中维护,用户对象不掌握成员列表。
        private final Map<String, User> users = new LinkedHashMap<>();
 
        User join(String name) {
            if (name == null || name.isBlank() || users.containsKey(name)) {
                throw new IllegalArgumentException("用户名无效或重复");
            }
            User user = new User(name, this);
            users.put(name, user);
            return user;
        }
 
        void leave(User user) {
            // 同时匹配名称和对象,避免误删同名的新成员。
            users.remove(user.name, user);
        }
 
        @Override
        public void send(User sender, String message) {
            Objects.requireNonNull(message);
            // 只有当前房间中的同一用户对象可以发送。
            if (users.get(sender.name) != sender) {
                throw new IllegalStateException("用户不在当前房间");
            }
            // 取得本轮接收者快照,再逐个转发。
            List<User> recipients = new ArrayList<>(users.values());
            for (User recipient : recipients) {
                // 转发规则:本示例不把消息发回发送者自身。
                if (recipient != sender) {
                    recipient.receive(sender.name, message);
                }
            }
        }
    }
 
    public static void main(String[] args) {
        ChatRoom room = new ChatRoom();
        User lin = room.join("小林");
        User chen = room.join("小陈");
        User wang = room.join("小王");
        lin.send("开始学习 Java 设计模式");
        room.leave(wang);
        chen.send("先看对象之间的关系");
        try {
            wang.send("我已经离开");
        } catch (IllegalStateException e) {
            System.out.println(e.getMessage());
        }
    }
}

房间维护成员和转发规则,参与者只依赖 ChatMediator。如果增加静音、分组或私聊,可以在协调边界内引入相应规则,避免每个用户分别维护同样逻辑。

耦合集中后的成本

若 N 个对象都直接持有其他对象,最多可能有 N×(N−1) 条有向引用关系;通过中介者可以减少参与者之间的直接引用。这只描述连接关系,业务交互本身的复杂度仍然存在。

中介者可能逐渐变成包含所有规则的大类。可以按交互领域划分房间、表单协调器或任务调度器,把复杂规则再交给独立协作者。单个协调入口也可能成为并发或性能瓶颈,需要明确线程和故障语义。

中介者、观察者与外观

观察者重点是一对多状态通知,发布者通常不关心订阅者如何反应;中介者了解参与者之间的协作规则,能够选择接收者并组织操作;外观面向外部调用者提供子系统入口。

事件总线可能同时具有通知和协调能力。判断时应观察业务规则由谁维护、谁选择接收者,以及参与者是否把交互职责交给中心对象。

九种模式的选择与合作

需求模式Java 中常用的表达方式
状态变化后通知多个对象观察者监听器、属性事件、订阅句柄
替换一个独立算法策略接口、Lambda、Comparator
稳定流程开放特定步骤模板方法抽象类、final 流程和钩子
多个处理者有顺序和短路规则责任链处理者列表、proceed() 协议
操作需要排队、历史或调度命令命令对象、Runnable、Callable
对象阶段决定允许的操作状态状态对象或明确的枚举转换表
遍历过程独立于集合内部结构迭代器Iterable、Iterator
保存并恢复对象内部状态备忘录由原发器创建的快照
多个参与者通过规则协调交互中介者协调接口和参与者注册

一个订单流程可以由责任链校验请求,用策略计算运费,通过状态协议确认允许发货,再提交命令到执行器,最后发出观察者事件。是否需要组合这些模式,取决于每项职责是否有独立的变化或执行要求。

组合以后需要继续检查失败边界:校验失败不应执行命令,异步任务失败应能被观察,事件通知失败不应悄悄丢失业务状态。对象关系明确后,错误和生命周期协议同样要明确。

练习:验证协作协议

  1. 给价格发布者加入一个会抛异常的监听器,观察其他订阅者是否仍收到通知,再实现你需要的失败策略。
  2. 在责任链中记录进入和返回的顺序,分别运行成功、鉴权失败、参数失败三种请求。
  3. 对文本编辑器执行三次操作、两次撤销和一次新操作,确认重做栈符合历史分支语义。
  4. 对订单状态逐一尝试支付、发货和取消,检查代码与转换表是否一致。
  5. 给快照加入可变列表,检查修改原对象后历史内容是否保持原样;再决定所需的复制深度。
  6. 在聊天室增加静音规则,明确是限制发送还是接收,并观察用户对象是否需要知道其他成员。

上一章:结构型模式 · 返回 Java 设计模式