设计模式之观察者(Observer)模式

什么是观察者模式?
观察者模式是软件设计模式的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。.
该模式可以帮你的对象知悉现况,不会错过该对象感兴趣的事。对象甚至在运行时可决定是否要继续被通知。观察者模式是JDK中使用最多的模式之一,非常有用。
认识观察者模式
我们看看B站用户的关注是怎么回事:
①up主的任务是更新视频。
②用户关注某个up主后,只要他们发布了新视频,就会给你发送一条动态消息。只要你是up主的粉丝,你就会一直收到新的动态消息。
③有一天你发现你对这个up主不感兴趣了,不想再看这个up主,取消关注,你就不会再收到那个up主的动态消息了。
④只要up主一直更新视频,就会有人关注他,毕竟这莫名的时代你直播修床都会有人看,就是这么无聊。。。
出版者(up主)+订阅者(粉丝)=观察者模式
如果你了解B站的订阅是怎么回事了,其实就知道观察者模式是怎么回事,只是名称不太一样:出版者改称为“主题”(Subject
),订阅者改称为“观察者”(Observer
)。具体如下图:

观察者模式的日常行为
有一天你
突然跑过来告诉主题,你想当一个观察者。你
其实想说的是:我对你的数据感兴趣,一有变化请通知我,如下图:

当主题对象同意了
你
的申请,你就成为正式的观察者了。现在你
可以静候通知,等着新的视频发布。一旦接收到新通知,就会得到一个数据。
这样主题也有了新数据(粉丝数+1)!其他观察者粉丝也会收到通知:主题已经改变了(你关注的up主又涨粉丝了)。
当有一天,1号粉丝对象要求从观察者中把自己除名,1号粉丝已经观察此主题太久,不想关注这个up主了,厌倦了,所以决定不再当个观察者。如下图所示:

1号粉丝对象脱粉了!主题对象知道1号粉丝的请求之后,把它从观察者中除名,如下图:

主题有了新的数据(粉丝数-1)。除了1号粉丝之外,每个观察者都会收到通知(up主粉丝-1)。
定义观察者模式
当你试图回想起观察者模式时,可以想想华农吃竹鼠的视频,你曾经在B站订阅过他,你是订阅者,他是出版者。因此观察者模式被定义成:
观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。

主题和观察者定义了一对多的关系。观察者依赖于此主题,,只要主题状态一有变化,观察者就会被通知。根据通知的风格,观察者可能因此新值而更新。
实现观察者模式的方法不只一种,但是以包含Subject
与Observer
接口的类设计的做法最常见。类图如下:

具体的主题是具有状态变量的对象,并且可以控制这些状态变量。也就是说, 有“一个”具有状态变量的主题。另一方面,观察者使用这些状态变量,虽然这些状态变量并不属于他们。有许多的观察者,依赖主题来告诉他们状态何时改变了。这就产生一个关系:“一个”主题对“多个”观察者的关系。
因为主题是真正拥有数据的人,观察者是主题的依赖者,在数据变化时更新,这样比起让许多对象控制同一份数据来,可以得到更干净的的OO设计。
作用:松耦合
观察者模式提供了一种对象设计,让主题和观察者之间松耦合。
为什么呢?
关于观察者的一切,主题只知道观察者实现了某个接口(也就是Observer接口)。主 题不需要知道观察者的具体类是谁、做了些什么或其他任何细节。
任何时候我们都可以增加新的观察者。因为主题唯一依赖的东西是一个实现Observer
接口的对象列表,所以我们可以随时增加观察者。事实上,在运行时我们可 以用新的观察者取代现有的观察者,主题不会受到任何影响。同样的,也可以在任何时候删除某些观察者。
有新类型的观察者出现时,主题的代码不需要修改。假如我们有个新的具体类需要当 观察者,我们不需要为了兼容新类型而修改主题的代码,所有要做的就是在新的类里 实现此观察者接口,然后注册为观察者即可。主题不在乎别的,它只会发送通知给所 有实现了观察者接口的对象。
我们可以独立地复用主题或观察者。如果我们在其他地方需要使用主题或观察者,可 以轻易地复用,因为二者并非紧耦合。
改变主题或观察者其中一方,并不会影响另一方。因为两者是松耦合的,所以只要他 们之间的接口仍被遵守,我们就可以自由地改变他们。
设计原则:为了交互对象之间的松耦合设计而努力。
松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化, 是因为对象之间的互相依赖降到了最低。
通过气象观测应用了解观察者模式
现在需要你写一个气象观测应用,该应用由WeatherData
对象(可观察的主题对象)负责追踪目前的天气状况(温度、湿度、气压),且有3个布告板(观察者对象)分别显示目前的状况(温度、湿度、气压)、气象统计(平均温度,最高低温度)及简单的预报(晴、雨、雪等)。当WeatherObject
对象(主题对象)获得最新的测量数据时,将注册了WeatherObject
对象(可观察的主题对象)的三种布告板(观察者对象)实时更新。
理清思路
此系统中的三个部分是气象站(获取实际气象数据的物理装置)、WeatherData对象(追踪来自气象站的数据,并更新布告板)和布告板(显示目前天气状况给用户看),如下图:

WeatherData
对象知道如何跟物理气象站联系(),以取得更新的数据。数据改变后WeatherData
对象会随即更新三个布告板的显示:目前状况(温度、湿度、气压)、气象统计和天气预报。那么WeatherData
对象应该有下面的特点:
- WeatherData类具有3个getter方法,可以取得三个测量值:温度、湿度与气压。
- 当新的测量数据备妥时,该类的measurementsChanged()方法就会被调用(我们不在乎此方法是如何被调用的,我们只在乎它被调用了)。
- 我们需要实现三个使用天气数据的布告板对象:“目前状况”布告、“气象统计”布告、“天气预报”布告。一旦WeatherData有新的测量数据,这些布告对象必须马上更新。
- 此系统必须可扩展,让其他开发人员建立定制的布告板对象, 用户可以随心所欲地添加或删除任何布告板。目前初始的布告板有三类:“目前状况”布告、“气象统计”布 告、“天气预报”布告。
那么我们如何使用观察者模式实现这个应用呢?我们知道观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。此处,我们的WeatherData
类正是此处所说 的“一”,而我们的“多”正是使用天气观测的各种布告板对象。
WeatherData
对象的确是有状态,包括了温度、湿度、气压,而这些值都会改变,当这些观测值改变时,必须通知所有的布告板,好让它们各自做出处理。
如果我们把WeatherData
对象当作主题,把布告板当作观 察者,布告板为了取得信息,就必须先向WeatherData
对象注册。一旦WeatherData
知道有某个布告板的存在,就会适时地调用布告板的某个 方法来告诉布告板观测值是多少。
每个布告板都有差异,这也就是为什么我们需要一个共同的接口的原因。尽管布告板的类都不一样,但是它们都应该实现相同的接口,好让WeatherData
对象能够知道如何把观测值送给它们。所以每个布告板都应该有一个大概名为update()的方法,以供 WeatherData对象调用。
因此,设计图如下:

实现气象站
有了设计图,我们就可以正式编码了。首先,我们建立3个接口:
//主题接口
public interface Subject {
//注册观察者
void registerObserver(Observer o);
//移除观察者
void removerObserver(Observer o);
//通知所有观察者
void notifyObservers();
}
//观察者接口
public interface Observer {
//当气象观测值改变时,主题会把这些状态值当作 方法的参数,传送给观察者,
//因为把观测值直接传入观察者不明智,这些观测值的种类和数量未来可能会变化,所以后期还会优化该类
void update(float temperature,float humidity,float pressure);
}
//具体信息显示接口
public interface DisplayElement {
//当布告板需要显示时, 调用此方法。
void display();
}
编写主题接口的实现类:
import ...
public class WeatherData implements Subject {
//观察者集合
private List observers;
//湿度
private float temperature;
//温度
private float humidity;
//气压
private float pressure;
public WeatherData() {
this.observers = new ArrayList();
}
//注册观察者
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
//移除观察者
@Override
public void removerObserver(Observer o) {
int i = observers.indexOf(o);
if (i >= 0) {
observers.remove(i);
}
}
//通知所有观察者
@Override
public void notifyObservers() {
for (int i = 0; i < observers.size(); i++){
Observer observer = (Observer) observers.get(i);
observer.update(temperature,humidity,pressure);
}
}
//测量结果改变则调用通知所有观察者方法
public void measurementsChanged(){
notifyObservers();
}
//模拟手动设置测量结果(湿度,温度,气压)
public void setMeasurements(float temperature,float humidity,float pressure){
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
}
编写观察者接口的实现类之一(当前状况):
public class CurrentConditionsDisplay implements Observer, DisplayElement {
//湿度
private float temperature;
//温度
private float humidity;
//气压
private float pressure;
//天气数据对象
private Subject weatherData;
public CurrentConditionsDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}
@Override
public void display() {
System.out.println("目前状况:湿度为" + temperature + "RH; 温度为:" + humidity + "度;气压为" + pressure + "帕斯卡");
}
}
编写测试类:
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay ccd = new CurrentConditionsDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
weatherData.setMeasurements(78, 90, 29.2f);
}
}
测试类运行结果:
目前状况:湿度为80.0RH; 温度为:65.0度;气压为30.4帕斯卡
目前状况:湿度为82.0RH; 温度为:70.0度;气压为29.2帕斯卡
目前状况:湿度为78.0RH; 温度为:90.0度;气压为29.2帕斯卡
主题和观察者间的状态差异
从前面一个例子中我们可以看到,当主题获得新的数据时,立马“推出”自己的状态信息给观察者,让观察者知道主题的最新情况。
但有时候观察者并不是想立即知道主题的情况,比如观察者在忙重要的事情。那么,主题既然可以通过主动“推出”数据,是否也能让观察者主动去向主题去“拉取”数据呢?这当然是可以的,然而主题却不应该公开它所有的状态数据,这样太危险了,可以折中一下让主题选择性的公开一部分数据,提供getter
方法即可,这样观察者就可以“拉取”走自己所需的状态数据。
或许会有人觉得这样不方便,还要每次都手动去取主题的状态数据,这要调集好多次才能收集想要的状态数据。然而众口难调,不管是主题主动“推出”状态数据,还是观察者主动想主题“拉取”状态数据,都有各自的优缺点。
Java内置的观察者模式对上面的两种做法都支持。
Java内置的观察者模式
Java API有内置的观察者模式。java.util
包内包含最基本的Observable
类与Observer
接口,这和我们的Subject
接口与Observer
接口很相似。 Observable
类与Observer
接口使用上更方便, 因为许多功能都已经事先准备好了。你甚至可以使用推(push
)或拉(pull
)的方式传送数据。
为了更了解java.uitl.Observer
和java.util.Observable
,看看下面的图,这是修改后的气象应用设计:

运作方式分析
Java内置的观察者模式运作方式,和我们在气象站中的实现类似,但有一些小差异。最明显的差异是WeatherData
(也就是我们的主题)现在扩展自Observable
类,并继承到一些增加、删除、通知观察者的方法(以及其他的方法,Java版本的用法步骤如下:
- 把对象变成观察者:如同以前一样,实现观察者接口(
java.uitl.Observer
),然后调用任何Observable
对象的addObserver
()方法。不想再当观察者时,调用deleteObserver
()方法就可以了。 - 主题送出通知:首先,你需要利用扩展
java.util.Observable
接口产生“可观察者”类,然后,需要两个步骤:①先调用setChanged()方法,标记状态已经改变的事实,②然后调用两种notifyObservers()
方法中的一个:notifyObservers()
或notifyObservers(Object arg)
- 观察者接收通知:同以前一样,观察者实现了更新的方法,但是方法的签名不太一样:
update(Observable o, Object arg)
,其中主题本身当作第一个变量, 好让观察者知道是哪个主题通知它的,第二遍变量arg
传入notifyObservers()
的数据对象。 如果没有说明则为空。
如果你想“推”(push
)数据给观察者,你可以把数据当作数据对象传送给notifyObservers(arg)
方法。否则,观察者就必须从可观察者对象中“拉”(pull
)数据。 如何拉数据?我们再做一遍气象站,你很快就会看到。
部分源码分析
其中的setChanged()
方法用来标记状态已经改变的事实,好让notifyObservers()
知道当它被调 用时应该更新观察者。如果调用notifyObservers()
之前没有先调用setChanged()
,观察者就“不会”被通知。从Observable
源码分析了解下:
private boolean changed = false;
//setChanged()方法把changed标志设 为true。
protected synchronized void setChanged() {
changed = true;
}
protected synchronized void clearChanged() {
changed = false;
}
//notifyObservers() 只会在 changed标为“true”时通知观 察者。
public void notifyObservers(Object arg) {
Object[] arrLocal;
synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}
for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}
通过setChanged()
方法可以让你在更新观察者时,更适当地通知观察者,这样会使程序有更多的弹性。比如,如果没有setChanged()
方法,气象站测量太过精确, 以致于温度计读数每十分之一度就会更新,这会造成WeatherData
对象持续不断地通知观察者,浪费了资源。如果我们希望半度以上才更新,就可以在温度差距到达半度时,调用setChanged()
(加个if
语句判断即可)进行有效的更新。 或许我们不会经常用到此功能,但是把这样的功能准备好,需要时就可以马上使用。如果此功能在某些地方对你有帮助,你可能也需要clearChanged()
方法,将changed
状态设置回false
。另外也有一个hasChanged()
方法, 告诉你changed
标志的当前状态。
改造原有气象站
首先,我们让WeatherData
对象继承Java自带的Observable
l类再进行改造:
import java.util.Observable;
public class WeatherData extends Observable {
//湿度
private float temperature;
//温度
private float humidity;
//气压
private float pressure;
public WeatherData() {
}
//测量结果改变时,只有先将changed状态改为true时,通知所有观察者方法才会被调用
public void measurementsChanged(){
setChanged();
notifyObservers();
}
//模拟手动设置测量结果(湿度,温度,气压)
public void setMeasurements(float temperature,float humidity,float pressure){
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
public float getTemperature() {
return temperature;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}
之后重做CurrentConditionsDisplay
对象:
import java.util.Observer;
public class CurrentConditionsDisplay implements Observer, DisplayElement {
//湿度
private float temperature;
//温度
private float humidity;
//气压
private float pressure;
//
private Observable observable;
//运用多态传入其Observable子类对象即可
public CurrentConditionsDisplay(Observable observable) {
this.observable = observable;
observable.addObserver(this);
}
//现在自己去主题对象中拿数据
@Override
public void update(Observable o, Object arg) {
if (o instanceof WeatherData){
WeatherData weatherData = (WeatherData) o;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
this.pressure = weatherData.getPressure();
display();
}
}
@Override
public void display() {
System.out.println("目前状况:湿度为" + temperature + "RH; 温度为:" + humidity + "度;气压为" + pressure + "帕斯卡");
}
}
运行测试类:
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay ccd = new CurrentConditionsDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
weatherData.setMeasurements(78, 90, 29.2f);
测试结果:
目前状况:湿度为80.0RH; 温度为:65.0度;气压为30.4帕斯卡
目前状况:湿度为82.0RH; 温度为:70.0度;气压为29.2帕斯卡
目前状况:湿度为78.0RH; 温度为:90.0度;气压为29.2帕斯卡
可以看到,测试结果和原来一样,只是内部定义不同。原来是自己定义的主题去“推送”数据给所有观察者,现在是用Java提供的类接口用观察者去“拉取”主题的数据(当然Java提供的也能推送数据,这里那样实现罢了)。
当然Java自带的观察者模式类和接口也有缺点,比如主题Observable
是一个“类”而不是一个接口,我们必须设计一个子类来实现它,如果该子类还要继承其他的父类,就无法做到了,因为Java不支持多继承,这限制了Observable
的复用能力。而且setChanged
方法被用protected
关键字保护起来了,只有你继承了Observable
类才能使用,无法将其组合到自己的对象中,违反了设计原则:多用组合,少用继承。
抉择:使用自定义还是Java提供的观察者模式?
如果能扩展Java自带的观察者模式API,它可能符合我们的需求,否则的话我们还是乖乖地向最开始一样实现一整套观察者模式。
实现up主和粉丝
假如由你来设计B站的up主(Uploader
)和粉丝(Fans
)之间的关系,由Uploader
对象负责接收相关信息(如up主姓名,所属分类,发布的视频信息),当Uploader
对象一发布新的视频时,Fans
对象就将收到一条新动态(你关注的up主某某更新了新的视频。。)当然,观察者可能不止粉丝一种,如果有赞助商,也可以算观察者(比如经常有在视频中拉赞助的。。。)
此系统中的三个部分是up主(可以填写实际的数据)、Uploader
对象(接收来自up主提供的数据)和粉丝(显示Uploader
对象更新的动态)。
注意:
此处up主和Uploader
对象代表的意义不同,一个是现实生活中的实体,一个是虚拟的对象。就好像人(up主)可以操纵程序,向其写入数据,因此程序(Uploader
对象)只相当于一个接收者。也就是说,Uploader
对象依赖于人(up主)提供的数据。
Uploader
对象知道如何跟up主建立联系,以获取更新的数据。Uploader
对象会随即更新观察者(粉丝、赞助商)的动态。
所以Uploader
对象应该3个getter
方法,可以获取up主名字,up主类别,发布的视频信息,还应该有一个upStateChanged
方法,当up主更新了新的数据时,该方法就会被调用。
我们需要实现订阅了up主的粉丝对象或赞助商对象,一旦Uploader
对象有了新的数据,这2个观察者就获得了新的动态。
此系统可以扩展让其他开发任意定制新的观察者
那么,我们该如何建立这个系统?当然是使用观察者模式了。请自己实现一下吧
参考资料
《HeadFirst设计模式》