C++观察者模式的实现

C++观察者模式的实现

观察者模式介绍

观察者模式是软件设计模式里面一种很常用又很重要的一种设计模式,观察者模式又叫做发布-订阅(Publish/Subscribe)模式。也就是主题对象(subject)发布通知,订阅该主题的多个观察者(observer)可以收到通知从而更新自己。

主题对象Subject发出通知时并不需要知道谁是它的观察者,也就是说具体观察者是谁,它根本不需要知道。而任何一个具体观察者不知道也不需要知道其他观察者的存在。

本质: 观察者模式本质是一个异步消息通知机制。
目的: 它可以起到多个对象通讯方之间的松耦合的目的。

什么时候使用观察者模式?

当一个对象的改变需要通知其他对象的时候,而且它不知道具体有多少对象有待通知的时候。

1. 经典观察者模式

经典观察者模式中的主题((subject))和观察者(observer),分别需要实现以下功能:

主题对象功能:

  • 添加订阅者
  • 删除订阅者
  • 更新主题内容
  • 通知订阅者

观察者功能:

  • 订阅主题
  • 获得主题的更新和通知

1.1 观察者模式类图

具体主题对象内部管理了一个观察者列表, 观察者订阅主题,也就是把自己“注册”给主题(调用主题对象的attach()方法),那么主题有内容更新时,就可以给所有登记过的观察者发出通知。

具体的观察实现对象,在主题更新时,会被通知并自动执行update()函数,所以观察者对象必须实现update()函数,并通过getmessage()来获得更新内容。

这个过程可以类比成,读者(观察者)订阅某本杂志(主题),当杂志有新的内容时,邮差小哥将新一期杂志投递给读者(通知),读者获得新的杂志后开始阅读。

1.2 实现

#include <stdint.h>
#include <iostream>
#include <string>
#include <map>
#include <mutex>

using namespace std;

class Observer
{
public:
    Observer(const string & name):m_name(name){}
    ~Observer(){}

    const string& getName()const {return m_name;}
    virtual void update(const string & name){cout << "!should'n go to here:"<< m_name << endl;}
    bool operator == ( const Observer& rhs){
        this->m_name == rhs.m_name ? true : false;
    }
protected:
    string m_name;
};

class Subject
{
public:
    Subject(const string & name):m_name(name){}
    ~Subject(){}
    
    const string& getName()const {return m_name;}
    bool attach(Observer *observer){
        lock_guard<mutex> lock_queue(mtx_map);
        if(nullptr == observer 
        || m_lstObserver.find(intptr_t(observer)) != m_lstObserver.end()) { //保证观察者对象不会重复添加
            return false;
        }
        m_lstObserver.insert({intptr_t(observer), observer}); 
        return true;
    }
    
    void detach(Observer *observer){
        if (nullptr != observer){
            lock_guard<mutex> lock_queue(mtx_map);
            m_lstObserver.erase(intptr_t(observer));      
        }
    }

    void notify(){
        for (auto& p : m_lstObserver)
            if (p.second){ p.second->update(m_name);}
    }

    /*****************/
    /*这里只是举例,在具体业务中需要更新的数据是多种多样的,
    /*此时可以选择合理的数据结构,并通过传入数据ID等更新和获取更精细的数据*/
    void setMessage(const string& message){
        if(m_message != message){
            m_message = message;
            notify();
        }
    }

    string getMessage() const{
        return m_message;
    }
    /****************/
    bool operator == (const Subject& rhs){
        this->m_name == rhs.m_name ? true : false;
    }
    
private:
    string m_name; 
    string m_message;
    map<intptr_t, Observer *> m_lstObserver;
    std::mutex mtx_map;     //考虑线程安全,对map和数据的访问需要加锁
};

class MagazineSubject : public Subject{
    using Subject::Subject;
};

class ReaderObserver : public Observer{
public:
    ReaderObserver(const string & name, Subject & subject)
    :Observer(name),m_subject(subject){
        m_subject.attach(this);
    };

    virtual void update(const string & name){cout << m_name << ": 收到了新的杂志" << m_subject.getMessage()  << endl;};
private:
    Subject & m_subject;
};

int main()
{
    MagazineSubject magazine1("中国地理");
    MagazineSubject magazine2("时代周刊");

    ReaderObserver readerA("读者A",magazine1);
    ReaderObserver readerB("读者B",magazine1);
    ReaderObserver readerC("读者C",magazine2);
    ReaderObserver readerD("读者D",magazine2);

    magazine1.setMessage("《鸡你太美》");
    magazine2.setMessage("《泰裤辣》");
    return 0;
}

这里的实现中,观察者获取数据的方式是,是主动从主题中拉取数据通过使用getMessage()这种统一接口。这么做的目的是为了让观察者能够灵活的获取自己所需的数据,而不是被动的被推送一大堆不是自己需要的数据,同时在增加新的观察者时,不需要修改update传参来满足不同的数据推送需求。

当然也可以是主题在调用update()时通过传参主动推送,这样不用主题内部保存多分数据的拷贝,降低复杂度和耦合,这两种方式各有优缺点。

2. 增强版观察者模式

从上面的介绍,大家可以看到的一个问题,就是观察者需要获得主题对象的引用,才能完成订阅的操作,也就是观察者依赖于主题(存在耦合)。就好比读者必须知道杂志的出版商,并到出版商那里去订阅。

同时,一个观察者可能还需要订阅多个主题,就像一个读者可能会订阅多份杂志一样, 当然在1.2实现章节的代码中,我们可以增加一个接口来样观察者订阅多个主题,但是这不是最终的解决方案。

针对以上问题,我们需要一个“增强版”的观察者模式,来进一步解决观察者和主题之间的相互依赖的问题。

这时候我们引入一个第三方的“杂志发布和订阅平台”, 杂志商只需要往平台上出版发布杂志信息,读者在平台上浏览所有的杂志信息,并根据自己的喜好来订阅多本杂志。杂志商不再需要把自己推销给具体的读者,读者也不需要找到具体的多个杂志出版商来订阅杂志。这时的平台充当中介的角色,这样就达到了彻底解耦的目的。

2.1 增强版观察者模式类图


由上图可以看出,xxxObserver类和xxxSubject类之间的关联或者依赖就不存在了,达到了松耦的目的

2.2 实现

在1.2章节的实现基础上,去掉在具体观察者对象构造函数中的主题对象引用传参,同时实现SubjectManage类即可

class SubjectManage
{
public:
    ~SubjectManage(){
    };

    static SubjectManage & Instance(){ static SubjectManage s_instance; return s_instance;}
    void AddSubject(Subject& subject){
        if (m_lstSubjects.find(subject.getName()) == m_lstSubjects.end()){
            m_lstSubjects.insert({subject.getName(), &subject});
        }
    }

    bool CreateSubCribe(Observer& observer, const string& sub_name){
        if (m_lstSubjects.find(sub_name) != m_lstSubjects.end()){
           return m_lstSubjects.at(sub_name)->attach(&observer);
        }
        return false;
    }

    bool deleteSubcribe(Observer& observer, const string& sub_name){
        if (m_lstSubjects.find(sub_name) != m_lstSubjects.end()){
            m_lstSubjects.at(sub_name)->detach(&observer);
        }
        return true;
    }

    string getMessage(const string& sub_name){
        if (m_lstSubjects.find(sub_name) != m_lstSubjects.end()){
            return m_lstSubjects.at(sub_name)->getMessage();
        }

        return string("error");
    }
private:
    SubjectManage(){m_lstSubjects.clear();};
    map<string, Subject *> m_lstSubjects;
};

class MagazineSubject : public Subject{
    using Subject::Subject;
};

class ReaderObserver : public Observer{
public:
    using Observer::Observer;
    virtual void update(const string & sub_name){cout << m_name
     << ": 收到了"<< sub_name <<"的杂志" << SubjectManage::Instance().getMessage(sub_name)  << endl;}
};

int main()
{
    MagazineSubject magazine1("中国地理");
    MagazineSubject magazine2("时代周刊");

    ReaderObserver readerA("读者A");
    ReaderObserver readerB("读者B");
    ReaderObserver readerC("读者C");
    ReaderObserver readerD("读者D");

    SubjectManage::Instance().AddSubject(magazine1);
    SubjectManage::Instance().AddSubject(magazine2);


    SubjectManage::Instance().CreateSubCribe(readerA, "中国地理");
    SubjectManage::Instance().CreateSubCribe(readerB, "中国地理");
    SubjectManage::Instance().CreateSubCribe(readerC, "中国地理");

    SubjectManage::Instance().CreateSubCribe(readerA, "时代周刊");
    SubjectManage::Instance().CreateSubCribe(readerB, "时代周刊");
    SubjectManage::Instance().CreateSubCribe(readerD, "时代周刊");

    magazine1.setMessage("《鸡你太美》");
    magazine2.setMessage("《泰裤辣》");
    return 0;
}

注意:上述示例代码基于C++11及以上的标准,编译时请增加-std=c++11参数

posted @ 2024-04-12 15:41  HL棣  阅读(10)  评论(0编辑  收藏  举报