目录
[十.出版者+订阅者 == 观察者模式](#十.出版者+订阅者 == 观察者模式)
一.专栏简介
本专栏是我学习《head first》设计模式的笔记。这本书中是用Java语言为基础的,我将用C++语言重写一遍,并且详细讲述其中的设计模式,涉及是什么,为什么,怎么做,自己的心得等等。希望阅读者在读完我的这个专题后,也能在开发中灵活且正确的使用,或者在面对面试官时,能够自信地说自己熟悉常用设计模式。
在上一章策略模式的学习之后,本章将开始观察者模式的学习。
二.背景介绍
在书中,我们需要建造基于Internet的气象观测站。这个气象站又基于WeatherData对象,这个对象追踪当前的天气状况(温度,湿度和气压)。我们要创建一个应用,该应用一开始提供三个显示元素(页面显示的内容和方式):当前状况,气象统计以及简单的预报。当WeatherData对象获得最新的测量数据时,三个显示元素都必须实时更新。
重点是这还是一个可扩展的气象站。我们允许其他开发人员后续编写自己的气象显示方式。所以,将来要便于添加新的显示方式,这一点很重要。悄悄话,这也是一种商业模式,客户要新的显示元素时,我们就对其进行收费。
三.气象监测应用的概况
系统有三个组件:气象站(获取实际气象数据的物理设备)、WeatherData对象(追踪来自气象站的数据并更新显示),以及向用户展示当前气象状况的显示方式。

WeatherData对象是客户提供给我们的,它知道如何与物理的气象站打交道,以获得更新的气象数据。我们需要适配WeatherData对象,以便它知道如何更新显示。客户一般会在WeatherData类中提示我们该如何做,也就是提示给我们编写代码的地方。
总结就是,我们需要基于WeatherData对象来更新三个显示:当前状况、气象统计和预报。
四.剖析WeatherData类
我们直接打开客户提供的源码看看吧,看WeatherData类:

使用C++代码将这个类编写出来,为了考虑后续页面的展示,我们使用qt这个框架编写,使用官方的Qt Creator编辑器。Qt版本并不重要,因为没用到特定版本的东西。代码如下:
widget.h:
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include "weatherData.h"
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
public slots:
void dataOnChanged();
private:
Ui::Widget *ui;
WeatherData* wd;
};
#endif // WIDGET_H
widget.cpp:
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
, wd(new WeatherData)
{
ui->setupUi(this);
connect(ui->temperatureVal, &QLineEdit::textChanged, this, &Widget::dataOnChanged);
connect(ui->humidityVal, &QLineEdit::textChanged, this, &Widget::dataOnChanged);
connect(ui->pressureVal, &QLineEdit::textChanged, this, &Widget::dataOnChanged);
}
Widget::~Widget()
{
delete ui;
}
void Widget::dataOnChanged()
{
wd->setTemperature((ui->temperatureVal->text()).toDouble());
wd->setHumidity((ui->humidityVal->text()).toDouble());
wd->setPressure((ui->pressureVal->text()).toDouble());
wd->measurementsChanged();
}
weatherData.h:
cpp
#ifndef WEATHERDATA_H
#define WEATHERDATA_H
#include <QDebug>
class WeatherData
{
public:
WeatherData();
// 无论何时WeatherData更新了值,measurementsChanged()方法被调用
void measurementsChanged();
// 设置最新的温度
void setTemperature(double temperature);
// 设置最新的湿度
void setHumidity(double humidity);
// 设置最新的压力
void setPressure(double pressure);
private:
// 返回最新的温度测量
double getTemperature();
// 返回最新的湿度测量
double getHumidity();
// 返回最新的压力测量
double getPressure();
double _temperature;
double _humidity;
double _pressure;
};
#endif // WEATHERDATA_H
weatherData.cpp:
cpp
#include "weatherData.h"
WeatherData::WeatherData():
_temperature(0), _humidity(0), _pressure(0)
{}
double WeatherData::getTemperature()
{
return _temperature;
}
double WeatherData::getHumidity()
{
return _humidity;
}
double WeatherData::getPressure()
{
return _pressure;
}
void WeatherData::measurementsChanged()
{
// 你的代码加在这里
qDebug() << "measurementsChanged被调用";
qDebug() << "温度:" << _temperature;
qDebug() << "湿度:" << _humidity;
qDebug() << "压力:" << _pressure;
}
void WeatherData::setTemperature(double temperature)
{
_temperature = temperature;
}
void WeatherData::setHumidity(double humidity)
{
_humidity = humidity;
}
void WeatherData::setPressure(double pressure)
{
_pressure = pressure;
}
main.cpp:
cpp
#include "widget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
widget.ui:
XML
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
<widget class="QWidget" name="">
<property name="geometry">
<rect>
<x>250</x>
<y>0</y>
<width>281</width>
<height>211</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,3">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="temperatureLabel">
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
</property>
<property name="text">
<string>温度:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="temperatureVal">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>0</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,3">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="humidityLabel">
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
</property>
<property name="text">
<string>湿度:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="humidityVal">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>0</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="1,3">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="pressureLabel">
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
</property>
<property name="text">
<string>气压:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="pressureVal">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>0</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>
页面展示如下:

我用Qt实现的逻辑就是一旦修改上面页面中温度,湿度或者气压的值,就会调用measurementsChanged方法,演示如下:


如上图,一旦修改三个值中的其中一个,就调用了measurementsChanged方法,也就是用手动修改温度,湿度或者气压来模拟温度,湿度或者气压一旦修改就调用measurementsChanged方法的逻辑。
tip:以上代码如果你不熟悉Qt是一下子看不懂的,那为什么我用Qt呢,因为我想像书中那样和图形化界面有交互。读者稍微熟悉一下Qt即可看懂。
五.我们的目标
我们知道需要实现一个显示,每一次WeatherData有新值,或者换句话说。每一次measurementsChanged()方法被调用时,WeatherData就更新这个显示,但怎么做呢?我们来全面考虑一下我们在尝试的做法:
- 我们知道WeatherData类有三个测量值的get方法:温度、湿度和气压。
- 我们知道,任何时候如果有了新的气象测量数据,measurementsChanged()方法会被调用(再说一次,我们不知道,或者说,不介意这个方法如何被调用;我们只知道它被调用就够了)。
- 我们需要实现三个使用气象数据的显示元素:一个当前状况显示,一个统计显示和一个预报显示。一旦WeatherData有新的测量,这些显示必须被更新。
- 为了更新显示,我们将添加代码到measurementsChanged()方法。
六.延展目标
我们还需要考虑未来,也就是可扩展性。就如同在上一篇策略模式的博客中说到的,软件开发的不变因素就是变化 。我们期望未来会有多于三个显示元素,所以要为额外添加的显示创建一个市场。
七.气象站的第一个实现,有误导
这是第一个我们的实现方式,将代码添加到WeatherData类的measurementsChanged()方法:
cpp
void WeatherData::measurementsChanged()
{
// 你的代码加在这里
// qDebug() << "measurementsChanged被调用";
// qDebug() << "温度:" << _temperature;
// qDebug() << "湿度:" << _humidity;
// qDebug() << "压力:" << _pressure;
double temp = getTemperature();
double humidity = getHumidity();
double pressure = getPressure();
currentConditionDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
}
为了与书中保持统一,我这里使用get方法获取这些变量,不让前面模拟measurementsChanged()自动被调用的逻辑影响这部分代码。
八.我们的实现到底有什么问题?
回想上一篇策略模式的博客,我们违反了哪些原则,哪一条又没有违反。重要的是,思考改变这段代码的后果。从上面那段代码,我们再来看一下:
cpp
currentConditionDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
由于针对具体实现编程,我们没有办法在不修改代码的情况下添加或移除其他显示元素。如果我们要在运行时添加或移除显示,也是不能做到的。所以这就是一种硬编码。
这段代码也有好的地方,至少,我们看起来在使用一个通用的接口来和显示元素打交道......它们都有一个update()方法,参数都是temp、humidity和pressure的值。
看起来像一个变化的区域,我们需要封装这里。
好好好,接下来就是观察者模式登场的时候,首先我们先专门学习观察者模式,然后将它运用到这个应用中。具体怎么用呢,下面先学完观察者模式再说。
九.认识观察者模式
通过报纸或杂志的订阅来引出这个模式:
- 报社开始运营,出版报纸。
- 你向特定报社订阅,每次他们有新报纸,就交付给你。只要保持订阅,你就会一直得到新报纸。
- 当你不想再看报纸时,就取消订阅,报纸就停止交付。
- 只要报社还在运营,就会一直有人、酒店、航班和其他企业订阅和取消订阅报纸。
十.出版者+订阅者 == 观察者模式
了解了报纸的订阅,我们就很大程度上理解观察者模式,只是我们把出版者叫做主题(SUBJECT ),把订阅者叫做观察者(OBSERVER)。
通过书中的插图我们来再看得仔细一点:

另外,注意一点,一个对象既可以是观察者,也可以是主题,就比如一个报社本身是一个主题,而这个报社又订阅了时尚杂志,那么这个报社就是这个时尚杂志的观察者。
十一.定义观察者模式
报纸订阅以及出版者和订阅者,是形象化观察者模式的一种好的方式。我们通常会看到观察者模式的定义像这样:
观察者模式:定义对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
稍后你会发现,观察者模式有几种不同的实现方式,但大多数都围绕着包括主题和观察者接口的类设计。
十二.观察者模式:类图

这里的依赖是怎样体现的呢?因为主题是数据的唯一拥有者,观察者是主题的依赖者,在数据变化时更新,这样比起允许许多对象控制同一份数据来,可以得到更干净的OO设计。
十三.松耦合的威力
当两个对象之间松耦合时,它们可以交互,但是通常对彼此所知甚少。正如我们所看到的,松耦合设计经常给我们带来很多弹性(不止一点点)。而且,正如结果所示,观察者模式是松耦合的一个很棒的例子。我们来看看,该模式如何达到松耦合:
- 首先,主题知道观察者的唯一一件事情是,它实现了某个接口(观察者接口)。
- 我们可以在任何时候添加新的观察者。
- 要添加新的类型的观察者,我们绝对不需要修改主题。
- 我们可以彼此独立地复用主题或观察者。
- 改变主题或观察者其中一方,不会影响另一方。
设计原则:尽量做到交互的对象之间的松耦合设计。
松耦合设计允许我们建造能够应对变化的,有弹性的OO系统,因为对象之间的互相依赖降到最低。
十四.设计气象站

十五.主题和观察者接口
cpp
#include <QDebug>
#include <QVector>
#include <unordered_set>
struct Subject;
struct Observer
{
void update(double temp, double humidity, double pressure);
protected:
Subject* subject;
};
struct Subject
{
void registerObserver(Observer* o)
{
observers.insert(o);
}
void removeObserver(Observer* o)
{
observers.erase(o);
}
virtual void notifyObserver() = 0;
protected:
std::unordered_set<Observer*> observers;
};
这里的update()方法是写死的想当于,当有更多参数时,就必须修改这个update方法。我们也可以写成无参的update()方法,然后观察者自己调用WeatherData的get方法获取需要的参数。
notifyObserver()是纯虚函数,所以这是个抽象类。因为需要调用observer对象中的update方法,而update方法需要子类WeatherData中的三个参数,温度,湿度和压力,所以让WeatherData重写的时候传入这些参数。
十六.在WeatherData中实现主题接口
现在我们带着观察者模式的思想改写WeatherData类。代码如下:
weatherData.h部分:
cpp
struct Subject
{
void registerObserver(Observer* o)
{
observers.insert(o);
}
void removeObserver(Observer* o)
{
observers.erase(o);
}
virtual void notifyObserver() = 0;
protected:
std::unordered_set<Observer*> observers;
};
class WeatherData : public Subject
{
public:
WeatherData();
// 无论何时WeatherData更新了值,measurementsChanged()方法被调用
void measurementsChanged();
// 设置最新的温度
void setTemperature(double temperature);
// 设置最新的湿度
void setHumidity(double humidity);
// 设置最新的压力
void setPressure(double pressure);
void notifyObserver() override
{
for(const auto& e : observers)
{
e->update(_temperature, _humidity, _pressure);
}
}
private:
double _temperature;
double _humidity;
double _pressure;
};
weatherData.cpp部分:
cpp
#include "weatherData.h"
WeatherData::WeatherData():
_temperature(0), _humidity(0), _pressure(0)
{}
void WeatherData::measurementsChanged()
{
notifyObserver();
}
void WeatherData::setTemperature(double temperature)
{
_temperature = temperature;
}
void WeatherData::setHumidity(double humidity)
{
_humidity = humidity;
}
void WeatherData::setPressure(double pressure)
{
_pressure = pressure;
}
十七.现在,我们来建造那些显示元素
它们分别是当前状况显示,统计显示和预报显示。代码如下:
cpp
class CurrentConditionsDisplay : public Observer
{
Q_OBJECT
public:
CurrentConditionsDisplay(Subject* s)
{
subject = s;
s->registerObserver(this);
}
void update(double temp, double humidity, double pressure) override
{
emit display(temp, humidity, pressure);
}
signals:
void display(double temp, double humidity, double pressure);
private:
};
class StatisticsDisplay : public Observer
{
Q_OBJECT
public:
StatisticsDisplay(Subject* s):
updateTimes(0)
{
subject = s;
s->registerObserver(this);
}
void update(double temp, double humidity, double pressure) override
{
(void)humidity;
(void)pressure;
avgTemp = (temp + avgTemp * updateTimes) / (updateTimes + 1);
++updateTimes;
emit display(avgTemp);
if(updateTimes == 2) subject->removeObserver(this);
}
signals:
void display(double temp);
private:
int updateTimes;
double avgTemp;
};
class ForecastDisplay : public Observer
{
Q_OBJECT
public:
ForecastDisplay(Subject* s)
{
subject = s;
s->registerObserver(this);
}
void update(double temp, double humidity, double pressure) override
{
emit display(temp, humidity, pressure);
}
signals:
void display(double temp, double humidity, double pressure);
private:
};
在update中,我们发射了自定义信号,最后再widget.cpp中绑定信号和建立槽函数的链接,最后将内容显示到界面上。
注意我们的统计显示元素,在更新第二次后取消了订阅,所以第二次之后将不会再更新数据。
十八.启动气象站
第一次输入7,当前状况显示和统计显示的温度或平均温度都变成了7。

第二次输入13,看看会有什么变化。

我们看到当前状况显示的温度变成了13,平均显示的温度变成了10,正如预期。下面我们更新第3次,统计显示的平均温度不应该变化,因为这时候它取消了对主题的订阅。

可以看到,变化正如预期。测试通过!!!
十九.Qt本身的观察者模式
Qt的信号与槽机制就是用观察者模式来编写的,我们在connect的时候实际上就是在做观察者向主题注册的步骤,当主题有变化(发送信号)时,就会调用槽函数这个观察者来进行响应。
二十.使用lambda表达式更改代码
cpp
connect(sd, &StatisticsDisplay::display, this, [this](double temp){
ui->temperatureVal_3->setText(QString::number(temp));
});
也就是在connect的时候将槽函数变成了一个lambda表达式,避免麻烦的槽函数声明与定义。
二十一.总结
- 观察者模式是一个以松耦合方式和一个对象集合沟通状态的模式。
- 观察者模式定义对象之间的一对多关系。
- 任何具体类型的观察者都可以参与该模式,只要它们实现观察者接口。
- 观察者是松耦合的,除了知道它们实现观察者接口之外,主题对它们的其他事情不知情。
- 使用该模式时,你可以从主题推或拉数据。