观察者模式

最近在研究 Qt 信号槽的源码实现,在看源码的过程中发现 Qt 信号槽很类似于观察者模式,所以先整理一篇观察者模式的文章

是什么

定义:当对象间存在一对多的依赖关系时,让多个观察者对象同时监听某一个主题对象,这个主题对象在状态发生改变时,会通知所有的观察者对象,使它们能够自动更新自己

下面看一下, 《大话设计模式》里对于观察者模式UML图的绘制

UML

为什么

将一个系统分割成一系列相互协作的类有一个很不好的副作用,那就是需要维护相关对象间的一致性。我们不希望为了维持一致性而使各个类紧密耦合,这样会给维护和扩展都带来不便

观察者模式的关键对象是观察者和被观察的对象,一个被观察的对象可以有任意数量依赖于它的观察者,一旦被观察的对象状态发生改变,所有的观察者都可以得到通知。 被观察者发出通知时,它并不需要知道谁是它的观察者,它只需要通知即可。同样,任何一个具体观察者不知道也不需要知道其他观察者的存在

所以观察者模式目的就是在解除耦合,让耦合的双方都依赖于抽象,而不是依赖于具体,从而使各自的变化都不会影响另一边的变化

当一个对象的改变需要同时改变其他对象,而且它不知道具体有多少对象有待改变时,可考虑使用观察者模式

怎么写

  • 因为被观察的对象需要通知所有观察者,所以观察者模式最核心的代码是,被观察者对象需要保存所有需要通知的观察者对象
  • 根据面向对象的思路,将具体的观察者和被观察者抽象化

来个栗子

先用《大话设计模式》解释观察者模式的例子是用 Java 实现的,我改用 C++ 实现

公司的门卫在看到老板回来之后,通知所有的划水的员工
员工A 关闭了打开的直播界面
员工B 放下了手上的手机

代码实现

根据 UML 图,先定义抽象的观察者,和抽象的主题(被观察者)

abstractclass.h

class Observer
{
public:
    virtual void Update() = 0;
};

class Subject
{
public:
    virtual void Attach(Observer *) = 0;
    virtual void Detach(Observer *) = 0;
    virtual void Notify() = 0;
};

门卫–继承被观察的主题,实现对应的虚函数

concretesubject.h

#include "abstractclass.h"
#include <QString>

class ConcreteSubject : public Subject
{
public:
    void Attach(Observer *pObserver);
    void Detach(Observer *pObserver);
    void Notify();

    void modifyStatus(QString status);

private:
    std::list<Observer *> m_ObserverList;
};

void ConcreteSubject::Attach(Observer *pObserver)
{
    m_ObserverList.push_back(pObserver);
}

void ConcreteSubject::Detach(Observer *pObserver)
{
    m_ObserverList.remove(pObserver);
}

void ConcreteSubject::Notify()
{
    std::list<Observer *>::iterator it = m_ObserverList.begin();
    while (it != m_ObserverList.end())
    {
        (*it)->Update();
        ++it;
    }
}

void ConcreteSubject::modifyStatus(QString status)
{
    if (status == "老板来了") {
        qDebug() << "老板来了,赶紧通知";
        Notify();
    }
    else
        qDebug() << status <<", 不是老板来了,不用通知了";
}

先假设只有员工A 和 员工B :

concreteobserver.h

#include "abstractclass.h"
#include <QDebug>

class ConcreteObserverA : public Observer
{
public:
    ConcreteObserverA(){}

    void Update()
    {
        qDebug() << "看直播的,赶忙关闭了直播界面";
    }

};

class ConcreteObserverB : public Observer
{
public:
    ConcreteObserverB(){}

    void Update()
    {
        qDebug() << "玩手机的,放下了手上的手机";
    }
};

主函数 mian() 里需要完成具体对象的初始化以及绑定观察者和被观察者

int main()
{
    ConcreteSubject doorman{};

    ConcreteObserverA employeeA{};
    ConcreteObserverB employeeB{};

    doorman.Attach(&employeeA);
    doorman.Attach(&employeeB);

    doorman.modifyStatus("小张迟到了");
    doorman.modifyStatus("老板来了");

    return 0;
}

运行结果:

"小张迟到了" , 不是老板来了,不用通知了
老板来了,赶紧通知
看直播的,赶忙关闭了直播界面
玩手机的,放下了手上的手机

简单总结一下

这个例子是最精简的观察者模式

所有员工都依赖于老板是否来了,而老板是否来了这个状态,门卫知道
所有员工先跟门卫吱一声,让门卫把他们的联系方式留一下 doorman.Attach(&employeeA)
当老板来了, 让门卫根据留的联系方式,通知他们就好

思考+1

被观察者对象实际是保存了 观察者对象的指针,通过主动调用函数 update() 函数,来 “通知观察者”,这样观察者和被观察者实际还是有一部分耦合的,另外那为什么不直接保存 观察者的 update() 的函数指针, 这样的话就可以进一步实现 解耦合

同样的例子,改一下实现, 这一次将抽象的类去掉了

在公共的头文件里先定义一个 函数指针

commondefine.h

#include <functional>
typedef std::function<void(void)> FUN_Notify;

门卫的部分代码,基本是一样的,只是将存储的观察者对象变成了函数指针

doorman.h

#include "commondefine.h"
#include <iostream>
#include <list>
#define UNUSED(x) (void)x;
using namespace std;

class DoorMan
{
public:
    void Attach(FUN_Notify func);
    void Detach(FUN_Notify func);
    void Notify();

    void modifyStatus(string status);

private:
    list<FUN_Notify> m_ObserverFuncList;
};

void DoorMan::Attach(FUN_Notify func)
{
    m_ObserverFuncList.push_back(func);
}

void DoorMan::Detach(FUN_Notify func)
{
    UNUSED(func);
//    m_ObserverFuncList.remove(func);
}

void DoorMan::Notify()
{
    std::list<FUN_Notify>::iterator it = m_ObserverFuncList.begin();
    while (it != m_ObserverFuncList.end())
    {
        (*it)();
        ++it;
    }
}

void DoorMan::modifyStatus(string status)
{
    if (status == "老板来了") {
        std::cout << "老板来了,赶紧通知" << std::endl;
        Notify();
    }
    else
        std::cout << status <<", 不是老板来了,不用通知了"<< std::endl;
}

最后员工部分的代码, 因为决定使用函数指针,所以直接可以这些类可以的函数命名可以形象,而且也不需要继承抽象观察者类了

employee.h

#include "commondefine.h"
#include <iostream>

using namespace std;

class EmployeeA
{
public:
    EmployeeA(){}

    void ClosePriview()
    {
        std::cout << "看直播的,赶忙关闭了直播界面" << std::endl;
    }

};

class EmployeeB
{
public:
    EmployeeB(){}

    void PutdownPhone()
    {
        std::cout << "玩手机的,放下了手上的手机" << std::endl;
    }
};

最后 main() 部分的代码

int main()
{
    DoorMan doorman{};

    EmployeeA employeeA{};
    EmployeeB employeeB{};

    auto closepriview = std::bind(&EmployeeA::ClosePriview, employeeA);
    auto putdownphone = std::bind(&EmployeeB ::PutdownPhone, employeeB);

    doorman.Attach(closepriview);
    doorman.Attach(putdownphone);

    doorman.modifyStatus("小张迟到了");
    doorman.modifyStatus("老板来了");
    return 0;
}

最后运行的结果是一样的,但是如果看代码细心的人可以看到

void DoorMan::Detach(FUN_Notify func)
{
    UNUSED(func);
//    m_ObserverFuncList.remove(func);
}

这一部分这样写主要的原因是, std::function 并没有重载 ==, list.remove() 的代码中有比较是否相等的操作,会导致编译报错, 所以这里就先注释了

另外插入的时候,也没有判断是否已经存在, 这2个 demo 其实还是有一些小问题的

思考+2

上面这个例子中已经使用 list<FUN_Notify> 来保存函数指针,而且这一组保存的默认是 老板来了 对应通知的函数指针,可是实际上门卫的工作可不止这么多,所以这次的例子进一步扩展一下

司的门卫在看到老板回来之后,通知所有的划水的员工
员工A 关闭了打开的直播界面
员工B 放下了手上的手机
门卫看到有人迟到,通知老板,老板记录之后训话

这个时候,还使用 list<FUN_Notify> 是不够的, 所以有以下几个思路

  • map<string, list<FUN_Notify>>
    map 键值来管理,map 的键是 具体的状态, map 的值存储所有需要通知的函数指针列表
  • vector<list<FUN_Notify>
    所有的状态可以做一份对应数组位置的映射,比如 老板来了 的函数指针列表对应数组的 0 位置, 员工迟到 的函数指针列表对应数组的 1 位置等等

这个例子的代码和上面2个例子和接近,主要就是如何管理函数指针,所以有兴趣的可以自己尝试一下,给一份伪代码

int main()
{
    DoorMan doorman{};

    EmployeeA employeeA{};
    EmployeeB employeeB{};
    auto closepriview = std::bind(&EmployeeA::ClosePriview, employeeA);
    auto putdownphone = std::bind(&EmployeeB::PutdownPhone, employeeB);
    doorman.Attach("老板来了",closepriview);
    doorman.Attach("老板来了", putdownphone);

    Boss boss{};
    auto punishment = std::bind(&Boss::Punishment, boss);
    doorman.Attach("迟到", punishment);

    doorman.modifyStatus("小张迟到了");
    doorman.modifyStatus("老板来了");
    return 0;
}

总结

观察者模式 本身的原理其实不复杂,通过将观察者和被观察的主题抽象化,具体的对象依赖于抽象的对象或者接口,从而实现解耦合,但是它的一些扩展还是很有意思的

  • Qt 的信号槽的机制很接近观察者模式的逻辑,很好的将信号的发送者和处理者解耦开来,它有点接近我后面思考里一些想法
    auto closepriview = std::bind(&EmployeeA::ClosePriview, employeeA);
    doorman.Attach("老板来了",closepriview);

这一部分其实很类似于 Qt 信号槽的 connect( doorman, 老板来了, employeeA, closepriview), 之后陆续有几篇关于 Qt 信号槽的完整逻辑分析

  • 观察者模式 还有一个名字 发布-订阅模式, 这个名字让我想到 rabbtimq 这个中间件也有一个类似的 发布-订阅模式, 所以在研究设计模式的路上又多了一篇新的文章,争取明天更新出来

上面 Demo 的 github 地址: https://github.com/catcheroftime/DesignPatterns/tree/master/ObserverPatterns