Skip to content

观察者模式 (Observer) —— 这里的消息最灵通

前言

在深入探讨观察者模式之前,想向大家推荐一个非常棒的开源项目:design-patterns-23。这个项目用最现代的技术栈重写了 23 种设计模式,非常适合实战学习,还配套了在线交互演示站,可以边读文章边动手玩。本文的实战案例灵感也来源于此。

1. 观察者模式是什么?

观察者模式(Observer Pattern),又叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式。它定义了对象之间的一对多依赖,当一个对象(Subject/Observable)改变状态时,它的所有依赖者(Observer)都会收到通知并自动更新。

简单来说,就是“别打电话给我,有事我会通知你”。

1.1 核心概念

  • Subject(主题/被观察者):维护一组观察者,提供添加、删除和通知观察者的方法。
  • Observer(观察者):定义一个更新接口,当收到主题的通知时被调用。
  • ConcreteSubject:具体的主题,状态发生改变时,向所有登记过的观察者发出通知。
  • ConcreteObserver:具体的观察者,实现抽象观察者定义的更新接口。

1.2 为什么我们需要它?

场景: 假设你正在开发一个气象站应用。气象数据(温度、湿度、气压)由 WeatherData 对象获取。当数据更新时,你需要更新三个布告板:

  1. 当前状况布告板
  2. 气象统计布告板
  3. 天气预报布告板

痛点: 如果不使用观察者模式,你可能会在 WeatherData 类里直接调用这三个布告板的 update() 方法。

java
public void measurementsChanged() {
    currentConditionsDisplay.update(temp, humidity, pressure);
    statisticsDisplay.update(temp, humidity, pressure);
    forecastDisplay.update(temp, humidity, pressure);
}

这样做的问题是:

  1. 高耦合WeatherData 必须知道具体的布告板类。
  2. 违反开闭原则:如果以后要增加一个“穿衣指南布告板”,你得修改 WeatherData 的代码。

解决方案: 让 WeatherData 成为 Subject,布告板成为 Observer。WeatherData 只管 notifyObservers(),不用管具体是谁在听。

2. 观察者模式的两种模型:推 (Push) vs 拉 (Pull)

在通知观察者时,有两种传递数据的方式:

2.1 推模型 (Push Model)

Subject 主动将数据“推”给 Observer。

  • 特点notifyObservers(Object data) 带参数。
  • 优点:Observer 不需要去 Subject 里取数据,直接拿到了。
  • 缺点:Subject 必须要知道 Observer 需要什么数据。如果 Observer A 需要温度,Observer B 需要湿度,Subject 只能把所有数据都推过去,或者推一个包含所有数据的对象。

2.2 拉模型 (Pull Model) —— 更灵活

Subject 只告诉 Observer "我更新了",Observer 收到通知后,自己去 Subject 里“拉”取自己需要的数据。

  • 特点notifyObservers() 不带参数,或者只带 this。Observer 的 update(Subject subject) 方法接收 Subject 引用。
  • 优点:按需索取,Subject 不需要关心 Observer 到底要啥。
  • 缺点:Observer 必须和 Subject 深度绑定(因为要调用 Subject 的 getter)。

3. Java 实战:电商订单通知系统

我们模拟一个电商场景:订单支付成功后,需要触发一系列动作:

  1. 短信服务:给用户发短信。
  2. 库存服务:扣减库存。
  3. 物流服务:通知仓库发货。

如果用观察者模式(使用拉模型),代码如下:

java
import java.util.ArrayList;
import java.util.List;

// 1. 观察者接口
interface OrderObserver {
    void update(OrderSubject subject);
}

// 2. 主题接口
abstract class OrderSubject {
    protected List<OrderObserver> observers = new ArrayList<>();

    public void attach(OrderObserver observer) {
        observers.add(observer);
    }

    public void detach(OrderObserver observer) {
        observers.remove(observer);
    }

    public void notifyObservers() {
        for (OrderObserver observer : observers) {
            observer.update(this);
        }
    }
}

// 3. 具体主题:订单系统
class OrderSystem extends OrderSubject {
    private String orderId;
    private double amount;
    private String status;

    public void paySuccess(String orderId, double amount) {
        this.orderId = orderId;
        this.amount = amount;
        this.status = "PAID";
        System.out.println(">>> 订单 " + orderId + " 支付成功,金额:" + amount);
        
        // 通知所有观察者
        notifyObservers();
    }

    public String getOrderId() { return orderId; }
    public double getAmount() { return amount; }
}

// 4. 具体观察者:短信服务
class SmsService implements OrderObserver {
    @Override
    public void update(OrderSubject subject) {
        if (subject instanceof OrderSystem) {
            OrderSystem order = (OrderSystem) subject;
            System.out.println("[短信服务] 发送短信给用户:订单 " + order.getOrderId() + " 支付成功!");
        }
    }
}

// 5. 具体观察者:库存服务
class InventoryService implements OrderObserver {
    @Override
    public void update(OrderSubject subject) {
        if (subject instanceof OrderSystem) {
            System.out.println("[库存服务] 扣减库存...");
        }
    }
}

// 6. 客户端
public class Client {
    public static void main(String[] args) {
        OrderSystem orderSystem = new OrderSystem();

        // 注册观察者
        orderSystem.attach(new SmsService());
        orderSystem.attach(new InventoryService());

        // 触发事件
        orderSystem.paySuccess("ORD-20231111", 99.00);
    }
}

输出

text
>>> 订单 ORD-20231111 支付成功,金额:99.0
[短信服务] 发送短信给用户:订单 ORD-20231111 支付成功!
[库存服务] 扣减库存...

4. 进阶:Spring 事件驱动模型 (ApplicationEvent)

在现代 Java 开发(尤其是 Spring Boot)中,我们很少手写 Observer 接口,而是直接使用 Spring 提供的事件发布/订阅机制。这是一种解耦更彻底的观察者模式。

4.1 核心组件

  • ApplicationEvent:事件对象(继承自 JDK EventObject)。
  • ApplicationEventPublisher:事件发布者(通常直接注入 ApplicationContext)。
  • @EventListener:事件监听者(注解在方法上)。

4.2 代码示例

Step 1: 定义事件

java
import org.springframework.context.ApplicationEvent;

public class OrderPaidEvent extends ApplicationEvent {
    private String orderId;

    public OrderPaidEvent(Object source, String orderId) {
        super(source);
        this.orderId = orderId;
    }

    public String getOrderId() {
        return orderId;
    }
}

Step 2: 发布事件

java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class OrderService {
    
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public void pay(String orderId) {
        System.out.println(">>> 支付完成,写入数据库...");
        // 发布事件,不用管谁在听
        eventPublisher.publishEvent(new OrderPaidEvent(this, orderId));
    }
}

Step 3: 监听事件

java
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class OrderListener {

    @EventListener
    public void sendSms(OrderPaidEvent event) {
        System.out.println("[短信] 发送短信通知:" + event.getOrderId());
    }

    @EventListener
    @Async // 还可以异步执行!
    public void notifyLogistics(OrderPaidEvent event) {
        System.out.println("[物流] 通知仓库发货:" + event.getOrderId());
    }
}

优点

  1. 完全解耦:发布者和监听者互不认识,只依赖 Event 类。
  2. 支持异步:配合 @Async,轻松实现异步处理,不会阻塞主线程(比如发短信慢,不能卡住支付接口)。
  3. 支持事务:使用 @TransactionalEventListener 可以控制在事务提交后才发送通知。

5. 源码中的观察者模式

5.1 JDK java.util.Observer (已过时)

JDK 1.0 就有了 Observer 接口和 Observable 类。 为什么过时?

  1. Observable 是一个而不是接口。Java 是单继承的,如果你的类已经继承了别的类(比如 HttpServlet),就没法继承 Observable 了,这极大地限制了复用性。
  2. setChanged() 方法是 protected 的,意味着你必须通过继承 Observable 才能调用它,无法通过组合的方式使用。

5.2 Java Beans PropertyChangeListener

Java GUI (Swing/AWT) 和 Java Beans 中大量使用了 PropertyChangeSupport,用于监听属性的变化。

5.3 Guava EventBus

Google Guava 库提供的 EventBus 是一个轻量级的进程内事件总线,使用 @Subscribe 注解标记监听方法,比 JDK 原生的更好用。

6. 观察者模式 vs 发布-订阅模式

虽然经常混用,但严格来说它们有区别:

特性观察者模式 (Observer)发布-订阅模式 (Pub-Sub)
耦合度较松(Subject 知道 Observer 列表)极松(Publisher 不知道 Subscriber 存在)
通信方式直接调用(Subject -> Observer)通过中间人(Publisher -> Broker -> Subscriber)
同步/异步通常是同步的通常是异步的(通过消息队列)
应用场景单体应用内部解耦分布式系统、微服务解耦

总结

  • Observer:你是我的粉丝,我发动态直接推给你。(B站关注)
  • Pub-Sub:我是广播电台,我把信号发到塔台(Broker),谁调到这个频道谁就听,我不知道谁在听。(RabbitMQ/Kafka)

7. 优缺点与避坑指南

7.1 优点

  1. 解除耦合:Subject 和 Observer 之间建立了一套触发机制,Subject 只依赖抽象的 Observer 接口。
  2. 支持广播通信:Subject 会向所有注册的 Observer 发送通知。

7.2 缺点

  1. 循环依赖:如果 A 观察 B,B 又观察 A,会触发死循环调用,导致系统崩溃。
  2. 性能问题:如果 Observer 非常多,或者 Observer 的 update 方法执行很慢,会阻塞 Subject 的运行(除非使用异步)。
  3. 不知“怎么变”:Observer 只知道 Subject 变了,但可能很难推断具体是哪里变了(尤其是在推模型下数据不全时)。

7.3 避坑指南:失效的监听器 (Lapsed Listener Problem)

问题: Subject 持有 Observer 的引用。如果 Observer 不再使用了,但忘记从 Subject 中 detach(取消注册),那么 Subject 依然强引用着 Observer,导致 Observer 无法被垃圾回收 (GC)。这就是内存泄漏。

解决

  1. 手动注销:一定要在合适的时机调用 detach
  2. 弱引用:Subject 内部使用 WeakReference 保存 Observer(Java 的 WeakHashMap),这样当 Observer 没有其他强引用时,GC 会自动回收它。

8. 总结

观察者模式是解耦神技。

  • 单体应用内,首选 Spring Events
  • 分布式系统中,请升级为 消息队列 (MQ) 实现发布-订阅。

9. 实验实操

design-patterns-web 项目中,我们通过一个 YouTube 频道订阅的场景来演示观察者模式。

  • 发布视频:点击 Channel 面板中的 "Post Video" 按钮,这就相当于 Subject 触发了一个事件。
  • 实时通知:下方的 Subscriber A 和 Subscriber B 列表会同时收到 "New video uploaded" 的通知。
  • 动态订阅:你可以随时点击 "Subscribe" 或 "Unsubscribe" 按钮,观察通知列表的变化,体验观察者列表的动态维护。 截图建议:点击 "Post Video" 后,截取包含 Channel 面板和下方多个 Subscriber 通知列表的完整界面,展示“一处变更,多处响应”的联动效果。