设计模式实战解读(四):观察者模式——事件驱动的解耦利器

本文是「设计模式实战解读」系列第四篇。系列文章统一按照 定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → FAQ 的结构展开,每篇聚焦一个模式讲透。


一句话定义

观察者模式(Observer):定义对象间一对多的依赖关系------当一个对象状态变化时,所有依赖它的对象自动收到通知并更新。也叫发布-订阅模式(Publish-Subscribe)。

归属:行为型模式。


一、没有观察者时的痛点

假设你在做一个流程引擎,流程实例状态变更(启动、完成、失败)后需要做很多事:

java 复制代码
public class FlowExecutor {
    
    public void onFlowComplete(FlowInstance instance) {
        // 1. 更新数据库状态
        flowDao.updateStatus(instance.getId(), "COMPLETED");
        
        // 2. 发送通知给用户
        notificationService.send(instance.getUserId(), "流程执行完成");
        
        // 3. 记录审计日志
        auditLogService.log(instance.getId(), "COMPLETED", System.currentTimeMillis());
        
        // 4. 更新监控指标
        metricsService.increment("flow.complete.count");
        metricsService.recordTime("flow.execution.duration", instance.getDuration());
        
        // 5. 触发下游流程(如果有)
        if (instance.hasDownstream()) {
            downstreamTrigger.trigger(instance.getDownstreamId());
        }
        
        // 6. 清理临时数据
        tempDataCleaner.clean(instance.getId());
        
        // 7. 发送 Webhook 回调
        webhookService.callback(instance.getCallbackUrl(), instance.toJson());
        
        // 还会继续增长...
    }
}

问题:

  1. FlowExecutor 知道太多东西了------它直接依赖了通知、日志、监控、下游触发、Webhook 等七八个服务
  2. 每加一个后置动作都要改 FlowExecutor------违反开闭原则
  3. 某个后置动作失败会影响整条链路------notificationService 超时,后面的全部阻塞
  4. 无法灵活配置------某些流程不需要 Webhook 回调,但代码里硬编码了
  5. 单元测试极其困难------要 mock 七八个依赖

核心诉求:发布者只负责"事情发生了",不关心"谁想知道"、"知道了要干什么"。


二、模式结构

复制代码
┌────────────────────────────┐
│    Subject (被观察者/发布者)  │
├────────────────────────────┤
│ - observers: List<Observer>│
├────────────────────────────┤
│ + addObserver(observer)    │
│ + removeObserver(observer) │
│ + notifyAll(event)         │
└──────────┬─────────────────┘
           │ 通知
           ↓
┌──────────────────────┐   ┌──────────────────────┐
│ Observer A (观察者)   │   │ Observer B (观察者)   │
├──────────────────────┤   ├──────────────────────┤
│ + onEvent(event)     │   │ + onEvent(event)     │
└──────────────────────┘   └──────────────────────┘

发布者和观察者之间是松耦合的------
发布者不知道具体有哪些观察者,只知道观察者实现了统一接口。

三、核心实现

3.1 手写基础版

java 复制代码
// 事件定义
public class FlowEvent {
    private final String flowId;
    private final String status;    // STARTED / COMPLETED / FAILED
    private final long timestamp;
    private final Map<String, Object> context;
    
    // constructor、getter 省略
}

// 观察者接口
public interface FlowEventListener {
    void onEvent(FlowEvent event);
    
    // 是否关心这个事件(过滤用)
    default boolean supports(FlowEvent event) {
        return true;
    }
}

// 事件发布器(被观察者)
public class FlowEventPublisher {
    private final List<FlowEventListener> listeners = new CopyOnWriteArrayList<>();
    
    public void addListener(FlowEventListener listener) {
        listeners.add(listener);
    }
    
    public void removeListener(FlowEventListener listener) {
        listeners.remove(listener);
    }
    
    public void publish(FlowEvent event) {
        for (FlowEventListener listener : listeners) {
            if (listener.supports(event)) {
                try {
                    listener.onEvent(event);
                } catch (Exception e) {
                    // 单个观察者异常不影响其他观察者
                    log.error("Listener {} failed", listener.getClass().getSimpleName(), e);
                }
            }
        }
    }
}

3.2 具体观察者实现

java 复制代码
// 观察者1:更新数据库状态
@Component
public class FlowStatusUpdater implements FlowEventListener {
    @Override
    public void onEvent(FlowEvent event) {
        flowDao.updateStatus(event.getFlowId(), event.getStatus());
    }
}

// 观察者2:发送通知
@Component
public class FlowNotificationListener implements FlowEventListener {
    @Override
    public void onEvent(FlowEvent event) {
        if ("COMPLETED".equals(event.getStatus()) || "FAILED".equals(event.getStatus())) {
            notificationService.send(event.getUserId(), buildMessage(event));
        }
    }
    
    @Override
    public boolean supports(FlowEvent event) {
        // 只关心完成和失败事件
        return "COMPLETED".equals(event.getStatus()) || "FAILED".equals(event.getStatus());
    }
}

// 观察者3:监控指标
@Component
public class FlowMetricsListener implements FlowEventListener {
    @Override
    public void onEvent(FlowEvent event) {
        metricsService.increment("flow." + event.getStatus().toLowerCase() + ".count");
    }
}

// 观察者4:审计日志
@Component
public class FlowAuditListener implements FlowEventListener {
    @Override
    public void onEvent(FlowEvent event) {
        auditLogService.log(event.getFlowId(), event.getStatus(), event.getTimestamp());
    }
}

3.3 改造后的 FlowExecutor

java 复制代码
public class FlowExecutor {
    private final FlowEventPublisher eventPublisher;
    
    public void onFlowComplete(FlowInstance instance) {
        // 只做一件事:发布事件
        eventPublisher.publish(new FlowEvent(
            instance.getId(),
            "COMPLETED",
            System.currentTimeMillis(),
            instance.getContext()
        ));
    }
}

从依赖 7 个服务变成依赖 1 个 EventPublisher------发布者彻底解耦。新增后置逻辑只需加一个 Listener 实现,不改 FlowExecutor。

3.4 结合 Spring 的事件机制

Spring 自带了观察者模式的基础设施:

java 复制代码
// 事件定义
public class FlowCompletedEvent extends ApplicationEvent {
    private final String flowId;
    private final String status;
    
    public FlowCompletedEvent(Object source, String flowId, String status) {
        super(source);
        this.flowId = flowId;
        this.status = status;
    }
}

// 发布事件(一行代码)
@Component
public class FlowExecutor {
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    public void onFlowComplete(FlowInstance instance) {
        eventPublisher.publishEvent(
            new FlowCompletedEvent(this, instance.getId(), "COMPLETED")
        );
    }
}

// 监听事件(注解驱动,零配置注册)
@Component
public class FlowMetricsListener {
    
    @EventListener
    public void handle(FlowCompletedEvent event) {
        metricsService.increment("flow.complete.count");
    }
}

@Component
public class FlowNotificationListener {
    
    @EventListener(condition = "#event.status == 'FAILED'")
    public void handleFailed(FlowCompletedEvent event) {
        notificationService.send(event.getFlowId(), "流程执行失败");
    }
}

// 异步监听(不阻塞主流程)
@Component
public class FlowAuditListener {
    
    @Async
    @EventListener
    public void handle(FlowCompletedEvent event) {
        auditLogService.log(event.getFlowId(), event.getStatus());
    }
}

Spring 事件机制的优势:

  • 不需要手动注册观察者(自动发现 @EventListener)
  • 支持条件过滤(condition SpEL 表达式)
  • 支持异步执行(@Async
  • 支持事务绑定(@TransactionalEventListener

四、真实应用场景

4.1 框架级应用

框架/技术 被观察者 事件 观察者
Spring ApplicationContext ContextRefreshedEvent 各种 Listener
Spring Boot SpringApplication ApplicationStartedEvent 自动配置模块
Java Swing JButton ActionEvent ActionListener
DOM (前端) HTMLElement click/input addEventListener
Vue.js 响应式数据 数据变化 视图更新函数
Redis Key 空间 过期/修改事件 订阅者
RocketMQ Topic 消息发布 Consumer Group

4.2 业务场景

业务事件 谁发布 谁订阅(干什么)
用户注册 UserService 发欢迎邮件、初始化配额、推送到 CRM
流程完成 FlowExecutor 通知用户、记日志、更新指标、触发下游
订单支付成功 PaymentService 更新订单状态、减库存、发短信、记账
配置变更 NacosClient 刷新 Bean、清缓存、重建连接池
连接器授权更新 OAuthService 刷新 Token 缓存、通知使用方、记审计日志
节点执行失败 NodeExecutor 重试调度、告警通知、快照写入

4.3 iPaaS 流程引擎中的事件驱动

在数环通 iPaaS 引擎中,流程执行状态变更是典型的事件驱动场景:

复制代码
FlowExecutionEngine(发布者)
    ├── 发布: FlowStartedEvent
    │       ├── 监控 Listener: 记录开始时间
    │       └── 限流 Listener: 检查并发配额
    │
    ├── 发布: NodeExecutedEvent
    │       ├── 日志 Listener: 持久化节点执行日志
    │       └── 进度 Listener: 更新流程执行进度
    │
    ├── 发布: FlowCompletedEvent
    │       ├── 通知 Listener: 回调用户 Webhook
    │       ├── 指标 Listener: 记录执行时长
    │       └── 清理 Listener: 释放临时资源
    │
    └── 发布: FlowFailedEvent
            ├── 恢复 Listener: 写入恢复快照
            ├── 告警 Listener: 发送失败通知
            └── 重试 Listener: 判断是否自动重试

引擎本身不关心"流程完成后要做什么"------它只负责把事件丢出去。所有后置逻辑通过 Listener 独立实现,可以按环境按租户灵活配置。


五、常见变种

5.1 同步 vs 异步观察者

java 复制代码
// 同步(默认):publish 方法内逐个调用观察者,阻塞直到全部完成
publisher.publish(event); // 所有 listener 执行完才返回

// 异步:每个观察者在独立线程中执行
public class AsyncEventPublisher {
    private final ExecutorService executor = Executors.newFixedThreadPool(4);
    
    public void publish(FlowEvent event) {
        for (FlowEventListener listener : listeners) {
            executor.submit(() -> {
                try {
                    listener.onEvent(event);
                } catch (Exception e) {
                    log.error("Async listener failed", e);
                }
            });
        }
    }
}

选择依据:

  • 同步:需要保证执行顺序、需要在同一事务内、需要知道是否全部成功
  • 异步:后置逻辑耗时长、不影响主流程、允许最终一致性

5.2 事件总线(EventBus)

Google Guava 提供的 EventBus 是观察者模式的工业级实现:

java 复制代码
// 创建事件总线
EventBus eventBus = new EventBus("flow-events");
// 异步版
AsyncEventBus asyncBus = new AsyncEventBus(executorService);

// 订阅(通过注解)
public class FlowMetricsSubscriber {
    @Subscribe
    public void onComplete(FlowCompletedEvent event) {
        metricsService.increment("flow.complete");
    }
}

// 注册
eventBus.register(new FlowMetricsSubscriber());

// 发布
eventBus.post(new FlowCompletedEvent(flowId));

5.3 跨进程观察者(消息队列)

进程内的观察者模式扩展到分布式------就是消息队列:

复制代码
进程内观察者                         跨进程观察者
───────────                         ───────────
EventPublisher                      RocketMQ Producer
    ↓ publish()                         ↓ send(topic, msg)
List<Listener>                      Consumer Group
    ↓ onEvent()                         ↓ consumeMessage()

从架构上看,RocketMQ / Kafka / RabbitMQ 本质就是跨进程的观察者模式。区别在于:

  • 进程内:低延迟、强一致、无持久化
  • 跨进程:高可用、可持久化、支持重试/死信

5.4 响应式流(Reactive Streams)

当事件是连续流时(如实时数据推送),观察者模式演变为响应式编程:

java 复制代码
// Project Reactor
Flux<FlowEvent> eventStream = flowEventSource.stream();

eventStream
    .filter(e -> "FAILED".equals(e.getStatus()))
    .buffer(Duration.ofSeconds(10))  // 每10秒批量处理
    .subscribe(batch -> alertService.batchAlert(batch));

六、优缺点

优点 缺点
发布者和订阅者松耦合 通知顺序不确定(同步模式下按注册顺序)
符合开闭原则(加订阅者不改发布者) 观察者太多时性能下降
支持动态注册/注销 调试困难(事件链路不直观)
天然适配事件驱动架构 可能导致内存泄漏(忘了注销)
可以轻松切换同步/异步 循环依赖难以发现(A 通知 B,B 又通知 A)

七、避坑指南

坑 1:观察者异常导致后续观察者不执行

java 复制代码
// 错误写法
public void publish(Event event) {
    for (Listener l : listeners) {
        l.onEvent(event); // 如果这里抛异常,后面的 listener 全部跳过
    }
}

// 正确写法
public void publish(Event event) {
    for (Listener l : listeners) {
        try {
            l.onEvent(event);
        } catch (Exception e) {
            log.error("Listener {} error", l.getClass().getSimpleName(), e);
            // 继续执行下一个
        }
    }
}

坑 2:观察者持有发布者引用导致内存泄漏

匿名内部类/Lambda 会隐式持有外部类引用。如果发布者生命周期长(如全局单例)、观察者生命周期短(如请求级对象),忘了 remove 会导致内存泄漏。

防御:用弱引用存储观察者,或者严格配对 add/remove。

坑 3:同步观察者阻塞主流程

一个慢观察者(比如发邮件耗时 3 秒)会把整个 publish 调用阻塞 3 秒。

防御 :耗时操作的观察者必须异步化(@Async 或独立线程池)。

坑 4:事件风暴

观察者内部又发布了新事件,导致递归/循环通知------最终 StackOverflow 或者 CPU 打满。

java 复制代码
// 危险:观察者内部又触发了事件
@EventListener
public void handle(FlowCompletedEvent event) {
    // 处理完成后发布新事件
    publisher.publishEvent(new AuditEvent(...)); // AuditEvent 的监听者又发布...
}

防御:事件链路最多 2-3 层,禁止循环发布。可以加深度计数器或者用异步打断循环。

坑 5:Spring @EventListener 在事务提交前执行

默认的 @EventListener 是在事务提交前执行的。如果你在 Listener 里读数据库查刚 INSERT 的数据,可能查不到。

防御 :用 @TransactionalEventListener(phase = AFTER_COMMIT) 确保事务提交后再执行。

坑 6:观察者顺序依赖

如果多个观察者之间有执行顺序要求(A 必须在 B 之前),不要依赖注册顺序------这不可靠。

防御 :用 @Order 注解(Spring)显式指定顺序,或者把有序逻辑放在同一个观察者内。


八、观察者模式 vs 发布-订阅模式

这两个术语经常混用,但有微妙区别:

维度 观察者模式 发布-订阅模式
耦合度 发布者知道观察者接口 发布者和订阅者完全不知道对方
中间层 无(直接通知) 有(EventBus / Broker)
典型实现 Java Observer、DOM Event RocketMQ、Redis PubSub、EventBus
通信 进程内同步 可跨进程异步
过滤 观察者自己过滤 中间层按 Topic 过滤

在实际讨论中,两者经常等同使用,不必过于纠结概念边界。


九、常见问题(FAQ)

Q:观察者模式和回调有什么区别?

A:回调是"一对一"(调用方注册一个回调函数),观察者是"一对多"(一个事件通知多个监听者)。回调适合"完成通知"场景,观察者适合"广播通知"场景。

Q:什么时候应该用消息队列代替进程内观察者?

A:三个信号:① 观察者需要跨服务(不在同一个 JVM);② 观察者需要可靠重试(进程内异常就丢了);③ 发布速率远大于消费速率(需要削峰)。满足任一条就应该上 MQ。

Q:观察者模式会影响性能吗?

A:同步模式下,N 个观察者的总耗时 = 各观察者耗时之和。如果观察者多且部分耗时,会显著拖慢 publish。解法:① 异步化耗时观察者;② 减少不必要的观察者(用 supports() 过滤);③ 批量通知代替逐条通知。

Q:Spring 的 @EventListener 是异步的吗?

A:默认是同步的(在 publishEvent 调用线程中执行)。要异步需要额外加 @Async + 开启 @EnableAsync。注意异步后事件的事务绑定和异常处理策略都需要调整。

Q:如何测试观察者?

A:① 单测观察者本身(传入 mock 事件,验证行为);② 集成测试发布→监听链路(发布事件,验证观察者的副作用)。不要把发布者和所有观察者放在一个测试里------那就变成集成测试了。

Q:事件溯源(Event Sourcing)和观察者模式的关系?

A:事件溯源把"状态变更"记录为不可变的事件序列,通过重放事件恢复状态。观察者模式是事件溯源的"通知层"------事件产生后,由观察者消费并产生副作用。两者是互补关系:事件溯源管"记录",观察者管"响应"。


十、小结

观察者模式的核心价值:把"事情发生了"和"要对此做什么"彻底解耦。

三个实践要点:

  1. 用 Spring @EventListener 而不是手写 Observer------框架帮你管理注册、支持条件过滤和异步
  2. 每个观察者用 try-catch 包裹------一个失败不能影响其他
  3. 耗时操作必须异步------否则慢观察者会拖垮整个事件链

设计模式不是银弹,但观察者模式几乎是所有事件驱动系统的基石。理解它,就理解了从 DOM 事件到消息队列到响应式编程的底层统一模型。


标签:#设计模式 #观察者模式 #Observer #发布订阅 #事件驱动 #EventListener #Spring #EventBus #Java #行为型模式 #解耦 #消息队列 #面向对象 #软件工程

相关推荐
我爱cope11 小时前
【Agent智能体7 | 智能体设计模式】
人工智能·设计模式
詩飛12 小时前
设计模式之建造者模式&模版模式、策略模式
后端·设计模式
IT空门:门主13 小时前
Java 设计模式实战:模板方法 + 工厂 + 策略模式重构支付系统
java·设计模式·策略模式
geovindu13 小时前
go: N-Barrier Pattern
开发语言·后端·设计模式·golang·屏障模式
这是谁的博客?1 天前
微服务架构设计模式深度解析:从拆分策略到容灾机制
微服务·设计模式·云原生·架构·架构设计·后端开发·分布式系统
fan_music1 天前
设计模式学习
c++·设计模式
乐观的山里娃1 天前
【后编码时代 06】Vibe Coding + Superpowers 完全不够
设计模式·软件工程·ai编程
likerhood1 天前
设计模式 · 责任链模式(Chain of Responsibility Pattern)
设计模式·责任链模式