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才通知,通知完再清掉。这样做的目的是让开发者可以控制通知的时机——你可以多次修改数据,最后只通知一次。
- Observable是一个类,不是接口。这意味着你的主题类必须继承它,但Java不支持多继承,限制了复用
- setChanged()方法被protected修饰,你不能直接调用,除非继承Observable或者把类放在同一个包下
- 通知顺序不可控。观察者被通知的顺序和注册顺序不一定一致
我曾经在一个金融项目中用了这套API,后来想改成自己的观察者实现,结果发现很多地方强依赖了Observable,改起来费了不少劲。所以现在我做新项目,更倾向于自己手写观察者模式,或者用Guava的EventBus。
用SVG梳理一下核心逻辑
下面这张图展示了观察者模式的基本结构,以及推模型和拉模型的区别。我画得比较简洁,重点看数据流向。
小结一下
观察者模式的核心就三件事:注册、通知、更新。主题维护一个观察者列表,状态变了就遍历通知。至于怎么通知,推还是拉,看你的场景。
JDK内置的Observer接口虽然方便,但局限性也不小。我个人建议,如果是新项目,自己写一个观察者接口反而更干净。代码量不大,但控制力强很多。
下一节我们会深入讲观察者模式的实际应用场景,包括事件驱动架构、消息队列的变体,以及如何避免内存泄漏。嗯,那个内存泄漏的问题,我当年可是花了一整天才排查出来……到时候细说。
公众号:蓝海资料掘金营,微信deep3321