参考书籍:
《HeadFirst 设计模式》
《设计模式-可复用面向对象软件的基础》
本文主要介绍对象行为型模式——Observer(观察者)模式,介绍的内容基于HeadFirst设计模式这本书,由于这本书是通过java编写,学习C++的朋友可能有所疑惑,因此本文借助GoF将其例子通过C++进行改编。
简介
观察者模式定义了对象间一对多的依赖关系,当一个对象(本文称其为目标对象)的状态发生改变时,所有依赖它的对象(本文称其为观察者对象)都会得到通知并被自动更新。
可能描述有些抽象,请看下图:
Observer模式描述了如何建立这种关系。这一模式中的关键对象就是目标(subject)和观察者(observer)。一个目标可以有任意数目的观察者,一但该目标状态发生变化,所有的观察者都会得到通知。而作为对这个通知的相应,每个观察者都会查询目标以使其状态与目标状态同步。这种交互又称为发布-订阅(publish-subscribe),目标是通知发布者,观察者就是订阅者。
描述
以下是观察者模式的类图:
问题
根据类图,就可以描述出之前的定义了:
1、观察者模式如何实现一对多的关系?
通过观察者模式,目标是拥有状态的对象,并且可以控制这些状态;观察者使用这些状态,但这些状态都不属于它自己,需要依赖目标提供。这就可以产生一(目标)对多(观察者)的关系。
2、目标和观察者的依赖如何产生?
目标是拥有数据者,而观察者是使用数据者,在数据发生变化时,比起让多个对象共用一份数据来说,是更良好干净的OO设计。
松耦合的优势
观察者模式提供了一种对象设计,让目标和观察者之间松耦合。
对于目标,它只知道观察者实现了某个接口(Observer的接口),目标并不需要知道观察者是谁,它做了什么或其他细节。
因此,在任何时候目标都可以增加或减少观察者,目标唯一依赖的是一个实现Observer接口的对象列表。
在新类型的观察者出现时,目标代码不需要做任何的改变。而观察者只需要实现Observer中要求的接口,并注册为观察者即可。
此时,我们可以独立复用目标和观察者,因为两者并非紧耦合,而是松耦合。改变目标或观察者一方,都不会对另一方产生任何影响,只要它们直接约定的接口被遵守,我们可以自由改变它们。
送耦合的设计建立的有弹性的OO系统,能够应付变化,因为对象之间的互相依赖降到了最低。
实现
接下来,我就以HeadFirst中描述的气象站项目,进行描述和实现观察者模式。
气象站项目的系统主要有三个部分:
- 气象站(物理装置,获取实际气象信息);
- WeatherData对象(追踪气象站数据,也就是目标对象);
- 布告板(显示目前天气状态,也就是观察者,不同的布告栏显示不同的天气)。
基类部分
我们根据观察者模式的描述,进行设计目标和观察者的基类部分。
观察者基类
首先,是观察者的基类Observer,该基类只有一个要求,只需提供update()函数的声明即可,因此我把它设计为纯虚类,如下:
class Observer {
public:
virtual void update(Subject& theChangeSubject) = 0;
};
我们再设计一个基类DisplayElement,用于输出观察者的数据变化,因为只需满足一个函数,因此设计成纯虚类。
class DisplayElement {
public:
virtual void display() = 0;
};
目标基类
接下来是目标的基类Subject,该基类需满足提供:
- 注册函数——registerObserver();
- 去注册函数——removeObserver();
- 通知函数——notifyObservers();
- 保存订阅的观察者的数据——list容器。
根据描述,Subject类可实现为如下:
#include <list>
#include <algorithm>
class Subject {
public:
virtual void registerObserver(Observer& o); //注册函数
virtual void removeObserver(Observer& o); //去注册函数
virtual void notifyObservers(); //通知函数
void setChange(); //用于控制通知函数
protected:
Subject() = default;
private:
bool change = false;
std::list<Observer*> observers; //保存订阅者的容器
};
void Subject::registerObserver(Observer& o) {
observers.emplace_back(&o); //将订阅者加入list,表示观察者已订阅
}
void Subject::removeObserver(Observer& o) {
auto ret = find(observers.begin(), observers.end(), &o); //借助标准库函数find,查找list中是否存在该观察者,若存在,则删除
if (ret != observers.end()) {
observers.erase(ret);
}
}
void Subject::notifyObservers() {
if (change) { //当对数据发生改变时,通知观察者
for (auto& observer: observers) {
observer->update(*this); //调用所有订阅观察者的update函数,以更新观察者的数据
}
change = false;
}
}
void Subject::setChange()
{
change = true;
}
至此,观察者模式的基类部分已设计完成,接下来以气象站项目为实例,介绍派生类的实现。
派生类部分
目标类
在之前的实现章节,讲到了目标是WeatherData类,该类控制气象数据,包括温度temperature、湿度humidity、压强pressure。在之前的类图中,可以看到Subject的派生类需提供设置数据和输出数据的接口,因此添加了setMeasurements()函数用于设置数据,getTemperature()、getHumidity()、getPressure()用于输出数据。
根据上述描述,实现如下所示:
#include "Subject.h"
class WeatherData: public Subject { //目标类继承自Subject
public:
WeatherData() = default;
WeatherData(float temp, float hum, float pre) :
temperature(temp), humidity(hum), pressure(pre) {}
void setMeasurements(float temp, float hum, float pre); //设置目标的数据
float getTemperature(); //与下述两个函数功能相同,对外提供数据
float getHumidity();
float getPressure();
private:
void measurementsChanged(); //用于通知观察者
private:
float temperature = 0; //设置为private,防止其他部分任意修改其参数
float humidity = 0;
float pressure = 0;
};
void WeatherData::measurementsChanged() {
setChange();
notifyObservers();
}
void WeatherData::setMeasurements(float temp, float hum, float pre)
{
temperature = temp;
humidity = hum;
pressure = pre;
measurementsChanged();
}
float WeatherData::getTemperature() {
return temperature;
}
float WeatherData::getHumidity() {
return humidity;
}
float WeatherData::getPressure() {
return pressure;
}
可以看到,WeatherData类只需要关心自己控制的数据如何设置和输出,与观察者模式相关的功能均由基类Subject提供。
观察者类
观察者需在构造时描述其关心的目标类,并对目标进行注册;而且需要实现Observer类的update()函数,以保证和目标类可以进行交互,实现DisplayElement的display()输出数据,以供观察变化。
根据以上描述,观察者类设计如下:
#include "WeatherData.h"
#include <iostream>
class CurrentConditionDisplay: public Observer, public DisplayElement {
public:
CurrentConditionDisplay(WeatherData& data): weatherData(&data) {
data.registerObserver(*this); //向weatherData注册
}
virtual void update(Subject& theChangeSubject) { //当Subject是自己关心的对象时,根据相应的目标,调用相应的接口
if (&theChangeSubject == weatherData) {
this->temperature = weatherData->getTemperature();
this->humidity = weatherData->getHumidity();
display();
}
}
virtual void display() { //输出观察者的数据信息
std::cout << "Current conditions: " << temperature
<< "F degrees and " << humidity << "% humidity" << std ::endl;
}
private:
float temperature;
float humidity;
WeatherData *weatherData; //保存自己关心的目标对象,这里只有WeatherData,可以观察多个对象,同样须在构造函数中向目标进行注册,在update函数中添加分支
};
至此,这个基于观察者模式的气象站项目设计完毕,可以看到,通过Subject和Observer类,目标类只需要继承Subject后,只关心自身的数据;观察者类只需要实现Obserer的update函数和向目标类进行注册。
测试
下面的测试函数,当然十分简单:
#include "WeatherData.h"
#include "CurrentConditionDisplay.h"
int main(int argc, char *argv[])
{
WeatherData weatherData; //目标对象
CurrentConditionDisplay currentDisplay(weatherData); //观察者对象1
CurrentConditionDisplay currentDisplay2(weatherData); //观察者对象2
weatherData.setMeasurements(80, 65, 30.4f); //目标对象的数据改变
weatherData.removeObserver(currentDisplay); //去注册观察者对象1
weatherData.setMeasurements(1, 1, 30.4f); //目标对象数据改变
return 0;
}
输出为:
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了。
总结
自此,观察者模式已介绍完毕,事实上,观察者模式具有两个模型:推/拉模型。本文介绍的是推模型——目标改变时即推送给各个观察者;拉模型就是观察者自身去拉取目标数据,目标不再通知观察者。
两种模型都有各自的优缺点,拉模型强调目标不知道它是观察者,推模型假定目标知道一些观察者需要的信息。推模型可能使得观察者相对难以复用,因为目标对观察者的假定可能并不总是正确的;而拉模型效率可能较差,因为观察者对象再没有目标对象帮助的情况下无法确定改变了什么。
要点
- 观察者模式定义了对象之间一对多的关系;
- 目标通过一个共同的接口更新观察者;
- 观察者和目标之间是松耦合的,目标不知道观察者的细节,只知道观察者提供的接口;
- 观察者模式有推模型和拉模型(然而,推模型被认为是更“正确”的)