基于C++的《Head First设计模式》笔记——观察者模式

目录

一.专栏简介

二.背景介绍

三.气象监测应用的概况

四.剖析WeatherData类

五.我们的目标

六.延展目标

七.气象站的第一个实现,有误导

八.我们的实现到底有什么问题?

九.认识观察者模式

[十.出版者+订阅者 == 观察者模式](#十.出版者+订阅者 == 观察者模式)

十一.定义观察者模式

十二.观察者模式:类图

十三.松耦合的威力

十四.设计气象站

十五.主题和观察者接口

十六.在WeatherData中实现主题接口

十七.现在,我们来建造那些显示元素

十八.启动气象站

十九.Qt本身的观察者模式

二十.使用lambda表达式更改代码

二十一.总结


一.专栏简介

本专栏是我学习《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表达式,避免麻烦的槽函数声明与定义。

二十一.总结

  • 观察者模式是一个以松耦合方式和一个对象集合沟通状态的模式。
  • 观察者模式定义对象之间的一对多关系。
  • 任何具体类型的观察者都可以参与该模式,只要它们实现观察者接口。
  • 观察者是松耦合的,除了知道它们实现观察者接口之外,主题对它们的其他事情不知情。
  • 使用该模式时,你可以从主题推或拉数据。
相关推荐
扑火的小飞蛾3 分钟前
【macOS】n8n 安装配置笔记
笔记·macos
三月微暖寻春笋4 分钟前
【和春笋一起学C++】(五十二)关于函数返回对象时的注意事项
c++·函数·const·返回对象·返回对象的引用
hssfscv5 分钟前
Javaweb学习笔记——JDBC和Mybatis
笔记·学习·mybatis
leiming67 分钟前
c++ transform算法
开发语言·c++·算法
羊小猪~~9 分钟前
数据库学习笔记(十八)--事务
数据库·笔记·后端·sql·学习·mysql
菩提祖师_11 分钟前
基于VR的虚拟会议系统设计
开发语言·javascript·c++·爬虫
YxVoyager19 分钟前
Qt C++ :QJson使用详解
c++·qt
小尧嵌入式20 分钟前
c++红黑树及B树B+树
开发语言·数据结构·c++·windows·b树·算法·排序算法
Wuliwuliii39 分钟前
贡献延迟计算DP
数据结构·c++·算法·动态规划·dp
苦藤新鸡41 分钟前
2.字母异位词分组
c语言·c++·力扣·哈希算法