观察者模式:一对多的依赖关系,状态变化自动通知

观察者模式,说白了就是“发布-订阅”机制。一个对象状态变了,所有依赖它的对象都能收到通知。我刚开始接触这个模式时,觉得它不就是个回调函数列表吗?后来在项目中用多了才发现,它的设计哲学远不止于此。

模式定义与核心思想

观察者模式定义了一种一对多的依赖关系。当一个对象(称为“主题”或“被观察者”)改变状态时,所有依赖它的对象(称为“观察者”)都会得到通知并自动更新。

你想想看,这像不像公众号和读者的关系?你关注了一个公众号,每次它发文章,你就能收到推送。你不关注,就收不到。这就是观察者模式最直观的理解。

核心角色:

  • Subject(主题):维护观察者列表,提供注册、注销和通知方法
  • Observer(观察者):定义更新接口,接收主题的通知
  • ConcreteSubject(具体主题):存储状态,状态变化时通知观察者
  • ConcreteObserver(具体观察者):实现更新接口,处理通知

为什么需要观察者模式?

我在项目中遇到过这样一个场景:一个电商系统的库存模块,库存变化时需要同步更新商品详情页、购物车、推荐系统等多个模块。如果直接在库存模块里写死这些依赖,代码会变得极其耦合,而且每增加一个依赖方就要改库存模块的代码。

观察者模式解决了这个问题。它让主题和观察者之间形成一种松耦合关系。主题只知道观察者实现了某个接口,但不知道观察者的具体实现。这样,观察者可以独立变化,也可以随时增加或删除。

适用场景:

  • 一个对象状态变化需要更新其他对象,且这些对象的数量不确定
  • 一个对象需要通知其他对象,但不想知道这些对象的具体细节
  • 需要实现事件驱动的系统,比如GUI中的按钮点击事件

代码示例:天气预报系统

我们来看一个实际的例子。假设我们要做一个天气预报系统,当天气数据更新时,需要通知多个显示面板。

// 观察者接口
interface Observer {
    void update(float temperature, float humidity, float pressure);
}

// 主题接口
interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObservers();
}

// 具体主题:天气数据
class WeatherData implements Subject {
    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;
    
    public WeatherData() {
        observers = new ArrayList<>();
    }
    
    public void registerObserver(Observer o) {
        observers.add(o);
    }
    
    public void removeObserver(Observer o) {
        observers.remove(o);
    }
    
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(temperature, humidity, pressure);
        }
    }
    
    // 当数据更新时调用
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        notifyObservers();
    }
}

// 具体观察者:当前天气显示
class CurrentConditionsDisplay implements Observer {
    private float temperature;
    private float humidity;
    
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }
    
    public void display() {
        System.out.println("当前温度: " + temperature + "°C");
        System.out.println("当前湿度: " + humidity + "%");
    }
}

// 具体观察者:统计显示
class StatisticsDisplay implements Observer {
    private float maxTemp = Float.MIN_VALUE;
    private float minTemp = Float.MAX_VALUE;
    private float tempSum = 0;
    private int numReadings = 0;
    
    public void update(float temperature, float humidity, float pressure) {
        tempSum += temperature;
        numReadings++;
        if (temperature > maxTemp) maxTemp = temperature;
        if (temperature < minTemp) minTemp = temperature;
        display();
    }
    
    public void display() {
        System.out.println("平均温度: " + (tempSum / numReadings));
        System.out.println("最高温度: " + maxTemp);
        System.out.println("最低温度: " + minTemp);
    }
}

观察者模式的变体:推模型 vs 拉模型

观察者模式有两种常见的实现方式。我个人习惯根据场景选择不同的模型。

特性 推模型 拉模型
通知方式 主题将数据推送给观察者 观察者主动从主题拉取数据
数据传递 在update方法中传递具体数据 传递主题对象本身,观察者自行获取
耦合度 较高,观察者依赖具体数据类型 较低,观察者只依赖主题接口
适用场景 观察者需要的数据比较固定 观察者需要的数据各不相同

上面的例子用的是推模型。如果改成拉模型,update方法就变成传递WeatherData对象,观察者自己决定要拿什么数据。

注意:推模型虽然简单直接,但容易造成数据冗余。如果观察者只需要部分数据,主题却把所有数据都推过去,会造成不必要的性能开销。我曾经在一个实时监控系统中就踩过这个坑,后来改成了拉模型才解决问题。

观察者模式的核心逻辑图

下面这张图展示了观察者模式的核心流程。主题维护观察者列表,状态变化时遍历列表通知所有观察者。

观察者模式核心逻辑 Subject(主题) 维护观察者列表 Observer 1 update() Observer 2 update() Observer 3 update() notifyObservers() 状态发生变化 触发 主题状态变化 → 遍历观察者列表 → 调用每个观察者的 update() 方法

Java内置的观察者模式

Java标准库其实已经提供了观察者模式的实现。java.util.Observable类和java.util.Observer接口就是现成的。但我建议你慎用。

为什么?因为Observable是一个类,不是接口。这意味着你的主题类必须继承它,但Java不支持多重继承。如果你的类已经继承了其他类,就没法用这个了。而且Observable的setChanged()方法需要手动调用,很多人会忘记。

我的建议:自己实现观察者模式反而更灵活。你可以把观察者管理逻辑封装成一个独立的类,通过组合的方式复用它。这样既避免了继承的局限,也让代码更清晰。

避坑指南

我曾经在一个大型项目中吃过观察者模式的亏。当时我们有一个事件总线,观察者数量非常多,每次通知都要遍历整个列表。结果某个观察者的update方法里抛了异常,导致后续观察者都收不到通知。

后来我们加了一个try-catch包裹每个观察者的调用,确保一个观察者出问题不会影响其他观察者。另外,如果观察者列表很大,可以考虑用线程池异步通知,避免阻塞主题线程。

注意事项:

  • 观察者中的异常不要影响主题的正常运行
  • 注意观察者的生命周期管理,避免内存泄漏
  • 如果观察者很多,考虑使用异步通知
  • 避免观察者之间产生循环依赖

观察者模式与其他模式的关系

观察者模式和发布-订阅模式很像,但有一个关键区别。发布-订阅模式通常有一个中间层(消息代理),观察者和主题之间不直接通信。而观察者模式是直接通信的。

另外,观察者模式也常和中介者模式配合使用。中介者模式可以管理多个观察者和主题之间的复杂关系,让它们之间的通信更加有序。

嗯,观察者模式就讲到这里。它看起来简单,但用好了能让你的系统架构更加灵活和可扩展。下次你在项目中遇到一对多的依赖关系时,不妨试试这个模式。


公众号:蓝海资料掘金营,微信deep3321