最近在研究 Qt
信号槽的源码实现,在看源码的过程中发现 Qt
信号槽很类似于观察者模式,所以先整理一篇观察者模式的文章
是什么
定义:当对象间存在一对多的依赖关系时,让多个观察者对象同时监听某一个主题对象,这个主题对象在状态发生改变时,会通知所有的观察者对象,使它们能够自动更新自己
下面看一下, 《大话设计模式》里对于观察者模式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