参考书籍:
《HeadFirst 设计模式》
《设计模式-可复用面向对象软件的基础》
本文主要介绍对象行为型模式——Observer(观察者)模式,介绍的内容基于HeadFirst设计模式这本书,由于这本书是通过java编写,学习C++的朋友可能有所疑惑,因此本文借助GoF将其例子通过C++进行改编。
简介
观察者模式定义了对象间一对多的依赖关系,当一个对象(本文称其为目标对象)的状态发生改变时,所有依赖它的对象(本文称其为观察者对象)都会得到通知并被自动更新。
可能描述有些抽象,请看下图:

Observer模式描述了如何建立这种关系。这一模式中的关键对象就是目标(subject)和观察者(observer)。一个目标可以有任意数目的观察者,一但该目标状态发生变化,所有的观察者都会得到通知。而作为对这个通知的相应,每个观察者都会查询目标以使其状态与目标状态同步。这种交互又称为发布-订阅(publish-subscribe),目标是通知发布者,观察者就是订阅者。
描述
以下是观察者模式的类图:
问题
根据类图,就可以描述出之前的定义了:
1、观察者模式如何实现一对多的关系?
通过观察者模式,目标是拥有状态的对象,并且可以控制这些状态;观察者使用这些状态,但这些状态都不属于它自己,需要依赖目标提供。这就可以产生一(目标)对多(观察者)的关系。
2、目标和观察者的依赖如何产生?
目标是拥有数据者,而观察者是使用数据者,在数据发生变化时,比起让多个对象共用一份数据来说,是更良好干净的OO设计。
松耦合的优势
观察者模式提供了一种对象设计,让目标和观察者之间松耦合。
对于目标,它只知道观察者实现了某个接口(Observer的接口),目标并不需要知道观察者是谁,它做了什么或其他细节。
因此,在任何时候目标都可以增加或减少观察者,目标唯一依赖的是一个实现Observer接口的对象列表。
在新类型的观察者出现时,目标代码不需要做任何的改变。而观察者只需要实现Observer中要求的接口,并注册为观察者即可。
此时,我们可以独立复用目标和观察者,因为两者并非紧耦合,而是松耦合。改变目标或观察者一方,都不会对另一方产生任何影响,只要它们直接约定的接口被遵守,我们可以自由改变它们。
送耦合的设计建立的有弹性的OO系统,能够应付变化,因为对象之间的互相依赖降到了最低。
实现
接下来,我就以HeadFirst中描述的气象站项目,进行描述和实现观察者模式。
气象站项目的系统主要有三个部分:
- 气象站(物理装置,获取实际气象信息);
- WeatherData对象(追踪气象站数据,也就是目标对象);
- 布告板(显示目前天气状态,也就是观察者,不同的布告栏显示不同的天气)。
基类部分
我们根据观察者模式的描述,进行设计目标和观察者的基类部分。
观察者基类
首先,是观察者的基类Observer,该基类只有一个要求,只需提供update()函数的声明即可,因此我把它设计为纯虚类,如下:
1 | class Observer { |
我们再设计一个基类DisplayElement,用于输出观察者的数据变化,因为只需满足一个函数,因此设计成纯虚类。
1 | class DisplayElement { |
目标基类
接下来是目标的基类Subject,该基类需满足提供:
- 注册函数——registerObserver();
- 去注册函数——removeObserver();
- 通知函数——notifyObservers();
- 保存订阅的观察者的数据——list容器。
根据描述,Subject类可实现为如下:
1 |
|
至此,观察者模式的基类部分已设计完成,接下来以气象站项目为实例,介绍派生类的实现。
派生类部分
目标类
在之前的实现章节,讲到了目标是WeatherData类,该类控制气象数据,包括温度temperature、湿度humidity、压强pressure。在之前的类图中,可以看到Subject的派生类需提供设置数据和输出数据的接口,因此添加了setMeasurements()函数用于设置数据,getTemperature()、getHumidity()、getPressure()用于输出数据。
根据上述描述,实现如下所示:
1 |
|
可以看到,WeatherData类只需要关心自己控制的数据如何设置和输出,与观察者模式相关的功能均由基类Subject提供。
观察者类
观察者需在构造时描述其关心的目标类,并对目标进行注册;而且需要实现Observer类的update()函数,以保证和目标类可以进行交互,实现DisplayElement的display()输出数据,以供观察变化。
根据以上描述,观察者类设计如下:
1 |
|
至此,这个基于观察者模式的气象站项目设计完毕,可以看到,通过Subject和Observer类,目标类只需要继承Subject后,只关心自身的数据;观察者类只需要实现Obserer的update函数和向目标类进行注册。
测试
下面的测试函数,当然十分简单:
1 |
|
输出为:
Current conditions: 80F degrees and 65% humidity
Current conditions: 80F degrees and 65% humidity
Current conditions: 1F degrees and 1% humidity
可以看到,当有两个观察者对象注册,在目标对象weahterData的数据发生改变时,两个观察者对象都会得到通知,并更新自己的相应数据,当观察者对象1去订阅时,再当目标发生改变时,只会通知观察者对象2,而不会再通知观察者1了。
总结
自此,观察者模式已介绍完毕,事实上,观察者模式具有两个模型:推/拉模型。本文介绍的是推模型——目标改变时即推送给各个观察者;拉模型就是观察者自身去拉取目标数据,目标不再通知观察者。
两种模型都有各自的优缺点,拉模型强调目标不知道它是观察者,推模型假定目标知道一些观察者需要的信息。推模型可能使得观察者相对难以复用,因为目标对观察者的假定可能并不总是正确的;而拉模型效率可能较差,因为观察者对象再没有目标对象帮助的情况下无法确定改变了什么。
要点
- 观察者模式定义了对象之间一对多的关系;
- 目标通过一个共同的接口更新观察者;
- 观察者和目标之间是松耦合的,目标不知道观察者的细节,只知道观察者提供的接口;
- 观察者模式有推模型和拉模型(然而,推模型被认为是更“正确”的)