书接上文观察者模式,今天介绍一个目的和观察者模式一样,但是在代码结构的上存在一定区别的设计模式–发布订阅模式
相似点
前提:当一个对象的改变需要同时改变其他对象,而且它不知道具体有多少对象有待改变时
目的:解除具体观察者或具体被观察的主题之间的耦合,让耦合的双方都依赖于抽象,而不是依赖于具体,从而使各自的变化都不会影响另一边的变化
区别
先看一下2种模式结构上的区别
观察者模式
发布订阅模式
其实从这个里可以直接看出最明显的区别是:
- 发布订阅模式在结构上比观察者模式多了一个中间件
- 观察者订阅的对象也是存在区别
- 观察者是直接关注主题,由主题直接通知
- 发布订阅模式观察者是向中间件订阅,由中间件将消息发送给感兴趣的的观察者
引入中间件的原因
进一步的解耦合
观察者模式的主题和观察者已经实现了松耦合,主题依赖于抽象的观察者,通过调用抽象观察者定义的通知接口,而不需要关心具体观察者这个借口是如何实现的
而在发布订阅模式中,将消息的发送者和观察者完全解耦了,发送者和观察者只需要知道中间件即可,发送者完全不知道有哪些观察者, 这就意味着,发布者的代码不需要维护观察者队列,也就不需要继承抽象的主题
因为系统的复杂度
上篇观察者模式思考+2
里提到一种情况
门卫看到老板来了通知所有员工,看到员工迟到了通知老板甚至更复杂的情况通知时
我当时的想法是用 map<string, list<Observer *>>
这样类似的方式,来管理情况和需要通知的对象, 观察者将被观察事件注册上来 doorman.Attach("老板来了",closepriview);
,事件发生变更时,这个 map
对象里寻找已注册该事件的观察者,将消息发送给对应的观察者。
这里的 map<string, list<Observer *>>
其实就是 中间件,观察者模式将这一部分的实现放置在被观察的主题对象的代码中了,发布订阅模式是将这部分管理的、调度的部分抽象成 中间件, 所以两种模式没有本质性的区别, 观察者部分的设计是一样的,发布者+中间件 约等于 主题
再举个例子:
现在观察者需要同时关注多个主题,比如老板这个观察者,门卫看到员工迟到通知老板,人事看到有人应聘通知老板,财务一到月底告诉老板公司营收状况
这里存在3个主题,每个主题都维护了一个观察者的列表,这一部分代码其实是重复的,结合可能更复杂的消息管理,将这个复杂的消息管理抽象成一个 中间件
,从而主题只要关心在状态发改变时,向中间件发送消息即可
整理一下之后得到的结论是:
复杂的消息分发的规则抽象成对象–中间件
,所有的发布者和观察者都向中间件发布消息和注册,从而简化逻辑
不是原因的原因
一个观察者关注多个主题这个例子中,此时抽象观察者接口就不能使用 virtual void Update() = 0;
, 此时 Update()
就需要传参了告诉观察者具体是什么发生了改变,假设这里用字符串或者枚举当参数virtual void Update(string mode) = 0;
,到这里2种模式还是一样的,但是注册接口调用的位置变了
观察者模式是在初始化好主题和观察者的类里,比如在 main()
的代码里完成观察者向主题的注册行为,此时对观察者而言,注册行为不太可控
发布订阅模式里观察者明确知道中间件的存在,观察者调用中间件的接口向中间件订阅自己感兴趣的内容,而且在 Update(string mode)
处理的时候也是根据自己订阅的内容有针对的处理,在自己观察者这个里,代码显得更加完整,可读性更高
应用场景的思考
其实引入中间件的原因里其实将应用场景也解释的差不多了:
- 系统越复杂,消息通知的种类越复杂,可以使用
发布订阅模式
- 中间件管理消息分发,此时可以在上面添加更多功能
- 收到的消息进行预处理,比如权限的验证等等
- 类似于
rabbitmq
的功能, 1.管理一个消息队列,失败了再次发送给观察者 2. 定时发送给观察者 等等
怎么写
还是以一个简单的例子说明:
公司的门卫在看到老板回来之后,通知所有的划水的员工
员工A 关闭了打开的直播界面
员工B 放下了手上的手机
先简单解释一下,观察者模式里,老板回来了是门卫的一个状态,门卫的状态变成老板来了,通知员工
这里如果用发布订阅模式引入中间件形式的话,门卫就相当于一个中间件,老板来了,跟门卫打声招呼,然后,门卫通知员工
跟观察者模式一样,需要一个抽象的观察者
abstractclass.h
class Observer
{
public:
virtual void Update() = 0;
};
先实现门卫的代码,门卫这个中间件,需要管理观察者,以及提供发布消息的接口
doorman.h
#include "abstractclass.h"
#include <iostream>
#include <list>
using namespace std;
class DoorMan
{
public:
void Attach(Observer *pObserver);
void Detach(Observer *pObserver);
void Publish(string info);
private:
void Notify();
private:
std::list<Observer *> m_ObserverList;
};
void DoorMan::Attach(Observer *pObserver)
{
m_ObserverList.push_back(pObserver);
}
void DoorMan::Detach(Observer *pObserver)
{
m_ObserverList.remove(pObserver);
}
void DoorMan::Notify()
{
std::list<Observer *>::iterator it = m_ObserverList.begin();
while (it != m_ObserverList.end())
{
(*it)->Update();
++it;
}
}
void DoorMan::Publish(string info)
{
if (info == "老板来了") {
std::cout << "老板来了,赶紧通知" << std::endl;
Notify();
}
else
std::cout << info <<", 不是老板来了,不用通知了"<< std::endl;
}
现在实现老板的代码,老板需要门卫打声招呼
boss.h
#include "doorman.h"
class Boss{
public:
Boss(DoorMan *doorman): m_pDoorman(doorman){}
void leaveCompany()
{
if (m_pDoorman)
m_pDoorman->Publish("老板回家了");
}
void comeCompany()
{
if (m_pDoorman)
m_pDoorman->Publish("老板来了");
}
private:
DoorMan * m_pDoorman;
};
员工的代码也很简单,主要是需要向门卫吱一声
concreteobserver.h
#include "abstractclass.h"
#include "doorman.h"
#include <iostream>
using namespace std;
class ConcreteObserverA : public Observer
{
public:
ConcreteObserverA(DoorMan *doorman){ doorman->Attach(this);}
void Update()
{
std::cout << "看直播的,赶忙关闭了直播界面" << std::endl;
}
};
class ConcreteObserverB : public Observer
{
public:
ConcreteObserverB(DoorMan *doorman){ doorman->Attach(this);}
void Update()
{
std::cout << "玩手机的,放下了手上的手机" << std::endl;
}
};
最后看一下 main() 里的代码
#include "boss.h"
#include "doorman.h"
#include "concreteobserver.h"
int main()
{
DoorMan doorman{};
ConcreteObserverA employeeA{&doorman};
ConcreteObserverB employeeB{&doorman};
Boss boss{&doorman};
boss.comeCompany();
boss.leaveCompany();
return 0;
}
最后的运行结果
老板来了,赶紧通知
看直播的,赶忙关闭了直播界面
玩手机的,放下了手上的手机
老板回家了, 不是老板来了,不用通知了
总结
- 本质上观察者模式和发布订阅模式是一样的,发布订阅模式是在规模和复杂度变化的情况下采取的新方案而已
- 发布订阅模式其实很类似于
MVC
,M
是发布者,V
是观察者,C
是中间件,有时间在搞一篇MVC
的文章 - 在例子中,老板和员工对象初始化的时候都传入
doorman
对象,但是门卫作为一个唯一实例,可以用 单例 的形式实现,很开心,下周又有新的内容了