6、观察者模式(上):观察者模式定义、推模型与拉模型、JDK内置的Observer接口

观察者模式到底在解决什么问题?

我先问大家一个问题:你写代码的时候,有没有遇到过这样的场景——某个对象状态变了,然后一堆其他对象都得跟着更新?

比如一个电商系统,用户下单了。库存要减、积分要加、短信要发、物流要生成。你总不能把这些逻辑全写在下单方法里吧?

我早年接手过一个老项目,那代码写得……一个订单创建方法里,硬生生塞了七八个业务模块的调用。后来需求变了,要加个优惠券发放。好嘛,又得改那个方法。改一次提心吊胆一次。

观察者模式就是来解决这个问题的。它的核心思想很简单:定义一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象状态发生变化时,它会通知所有观察者,让它们自动更新。

说白了,就是「发布-订阅」那一套。你订阅了我的公众号,我发文章了,你就能收到推送。我不需要知道你是谁,你也不需要天天来刷我有没有更新。

观察者模式的核心角色:
  • Subject(主题)——被观察的对象,维护观察者列表,提供注册和移除方法
  • Observer(观察者)——接收主题通知并做出响应
  • ConcreteSubject(具体主题)——状态变化时触发通知
  • ConcreteObserver(具体观察者)——实现具体的更新逻辑

推模型 vs 拉模型:两种通知方式

观察者模式里,主题通知观察者时,有两种做法。我习惯叫它们「推」和「拉」。你想想看,这其实是个很实际的问题——通知的时候,到底要带多少数据过去?

推模型

主题把变化后的数据一股脑推给观察者。观察者被动接收,不需要再回头问主题要数据。

// 推模型:主题把数据传给观察者
public interface Observer {
    void update(String temperature, String humidity, String pressure);
}

public class WeatherStation {
    private List<Observer> observers = new ArrayList<>();
    private String temperature;
    private String humidity;
    private String pressure;

    public void setMeasurements(String temp, String hum, String pressure) {
        this.temperature = temp;
        this.humidity = hum;
        this.pressure = pressure;
        // 推:把所有数据都传过去
        notifyObservers(temperature, humidity, pressure);
    }

    private void notifyObservers(String temp, String hum, String pressure) {
        for (Observer observer : observers) {
            observer.update(temp, hum, pressure);
        }
    }
}

推模型的优点很明显——观察者拿到数据就能用,不用再问。但缺点也在这里:如果观察者只需要其中一部分数据,它还是得接收全部。而且一旦数据字段变了,所有观察者的接口都得改。

我在项目中遇到过这种情况。一开始推模型传了三个字段,后来需求加了「风向」「风速」,好嘛,所有观察者实现类都得改一遍。那叫一个酸爽。

拉模型

主题只通知「我变了」,具体变了什么,观察者自己来取。观察者需要持有主题的引用,主动调用getter方法获取数据。

// 拉模型:主题只通知变化,观察者自己拉取数据
public interface Observer {
    void update(Subject subject);
}

public class WeatherStation implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private String temperature;
    private String humidity;
    private String pressure;

    public void setMeasurements(String temp, String hum, String pressure) {
        this.temperature = temp;
        this.humidity = hum;
        this.pressure = pressure;
        // 拉:只通知,不带数据
        notifyObservers();
    }

    private void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(this);  // 把主题自己传过去
        }
    }

    // 观察者通过getter自己拉取需要的数据
    public String getTemperature() { return temperature; }
    public String getHumidity() { return humidity; }
    public String getPressure() { return pressure; }
}

public class DisplayBoard implements Observer {
    @Override
    public void update(Subject subject) {
        WeatherStation station = (WeatherStation) subject;
        // 只拉取自己关心的数据
        String temp = station.getTemperature();
        System.out.println("当前温度:" + temp);
    }
}

拉模型更灵活。观察者只拿自己需要的数据,主题的接口也更稳定。但代价是观察者得知道主题的具体类型,不然没法调用getter。这增加了耦合。

我的建议:

如果观察者需要的数据比较固定,而且大部分观察者都需要全部数据,用推模型。如果观察者各取所需,或者数据字段经常变化,用拉模型更稳妥。

我个人更倾向于拉模型。虽然多写几行getter,但后期改起来省心太多。

JDK内置的Observer接口

Java从1.0开始就提供了观察者模式的支持。java.util.Observable是主题类,java.util.Observer是观察者接口。

嗯,这里要注意。这套API其实有点老了,Java 9之后甚至标记为弃用。但很多老项目还在用,而且它的设计思路值得学习。

import java.util.Observable;
import java.util.Observer;

// 具体主题:继承Observable
public class WeatherData extends Observable {
    private float temperature;
    private float humidity;

    public void setMeasurements(float temperature, float humidity) {
        this.temperature = temperature;
        this.humidity = humidity;
        // 必须先调用setChanged(),通知才会生效
        setChanged();
        // 推模型:传数据
        notifyObservers(new Object[]{temperature, humidity});
        // 拉模型:不传参,观察者自己调用getter
        // notifyObservers();
    }

    public float getTemperature() { return temperature; }
    public float getHumidity() { return humidity; }
}

// 具体观察者:实现Observer接口
public class CurrentConditionsDisplay implements Observer {
    @Override
    public void update(Observable o, Object arg) {
        if (o instanceof WeatherData) {
            WeatherData weatherData = (WeatherData) o;
            // 推模型:直接从arg取数据
            if (arg instanceof Object[]) {
                Object[] data = (Object[]) arg;
                float temp = (float) data[0];
                float hum = (float) data[1];
                System.out.println("温度:" + temp + ",湿度:" + hum);
            }
            // 拉模型:调用getter
            // float temp = weatherData.getTemperature();
            // float hum = weatherData.getHumidity();
        }
    }
}

这里有个坑,我踩过。setChanged()这个方法必须调用,否则notifyObservers()不会真正通知观察者。为什么这么设计?

因为Observable内部有个changed标志位。setChanged()把它设为true,notifyObservers()检查这个标志,为true才通知,通知完再清掉。这样做的目的是让开发者可以控制通知的时机——你可以多次修改数据,最后只通知一次。

JDK内置Observer的局限性:
  • Observable是一个类,不是接口。这意味着你的主题类必须继承它,但Java不支持多继承,限制了复用
  • setChanged()方法被protected修饰,你不能直接调用,除非继承Observable或者把类放在同一个包下
  • 通知顺序不可控。观察者被通知的顺序和注册顺序不一定一致

我曾经在一个金融项目中用了这套API,后来想改成自己的观察者实现,结果发现很多地方强依赖了Observable,改起来费了不少劲。所以现在我做新项目,更倾向于自己手写观察者模式,或者用Guava的EventBus。

用SVG梳理一下核心逻辑

下面这张图展示了观察者模式的基本结构,以及推模型和拉模型的区别。我画得比较简洁,重点看数据流向。

观察者模式核心结构 Subject(主题) - observers: List<Observer> + attach(Observer) + detach(Observer) + notifyObservers() 通知 Observer(观察者) + update(Subject, Object) ConcreteSubject - state: 具体数据 + getState(): 数据 ConcreteObserver + update(Subject, Object) // 处理通知 推 vs 拉 推模型: 主题把数据传给观察者 update(数据1, 数据2, ...) 优点:观察者直接用 缺点:接口易变,冗余 拉模型: 主题只通知「变了」 update(Subject) 优点:灵活,接口稳定 缺点:观察者需知道 主题的具体类型

小结一下

观察者模式的核心就三件事:注册、通知、更新。主题维护一个观察者列表,状态变了就遍历通知。至于怎么通知,推还是拉,看你的场景。

JDK内置的Observer接口虽然方便,但局限性也不小。我个人建议,如果是新项目,自己写一个观察者接口反而更干净。代码量不大,但控制力强很多。

下一节我们会深入讲观察者模式的实际应用场景,包括事件驱动架构、消息队列的变体,以及如何避免内存泄漏。嗯,那个内存泄漏的问题,我当年可是花了一整天才排查出来……到时候细说。


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