发布订阅模式

书接上文观察者模式,今天介绍一个目的和观察者模式一样,但是在代码结构的上存在一定区别的设计模式–发布订阅模式

相似点

前提:当一个对象的改变需要同时改变其他对象,而且它不知道具体有多少对象有待改变时
目的:解除具体观察者或具体被观察的主题之间的耦合,让耦合的双方都依赖于抽象,而不是依赖于具体,从而使各自的变化都不会影响另一边的变化

区别

先看一下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;
}

最后的运行结果

老板来了,赶紧通知
看直播的,赶忙关闭了直播界面
玩手机的,放下了手上的手机
老板回家了, 不是老板来了,不用通知了

总结

  • 本质上观察者模式和发布订阅模式是一样的,发布订阅模式是在规模和复杂度变化的情况下采取的新方案而已
  • 发布订阅模式其实很类似于 MVCM 是发布者, V 是观察者, C 是中间件,有时间在搞一篇 MVC 的文章
  • 在例子中,老板和员工对象初始化的时候都传入 doorman 对象,但是门卫作为一个唯一实例,可以用 单例 的形式实现,很开心,下周又有新的内容了