C++ 观察者模式

观察者模式

观察者模式解决什么问题?

经常看到一些对象调用其他对象的方法:因为实现复杂的任务通常需要几个对象一起合作完成,为了该目标,对象A为了调用对象B的方法,就必须知道对象B的存在及其接口。
最简单的办法是让A.cpp包含(include)B.h,然后直接调用B class方法。但这样做,做A中引入了B class,导致A和B编译时依赖,迫使两个类紧耦合。后果是,A class通用性减弱。如果A还调用了C class、D class,那么对A的改变会影响这三个紧耦合的类。而且,编译时紧耦合会导致用户不能在运行时给系统动态添加新的依赖。

观察者模式可以有效支持组件解耦,避免循环依赖。


观察者模式概念

观察者模式的意图定义为:对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都得到通知并被自动更新。

典型观察者模式有两个概念:主题(Subject),观察者(Observer),也称为发布者,订阅者。其通知方式是这样:用户通过观察者

观察者模式典型实现的UML见下图。主要工作是将Subject对Object的编译时依赖,转化为对接口类的依赖,从而实现解耦。

观察者模式典型实现

为主题,观察者定义抽象接口

#include <map>
#include <vector>

class IObserver
{
public:
    virtual ~IObserver() {}
    virtual void Update(int message) = 0;
};

class ISubject
{
public:
    ISubject();
    virtual ~ISubject();
    virtual void Subsribe(int message, IObserver *observer);  // 观察者订阅指定类型事件
    virtual void Unsubsribe(int message, IObserver *observer); // 观察者取消指定类型事件的订阅
    virtual void Notify(int message); // 通知已订阅指定事件的观察者

private:
    typedef std::vector<IObserver*> ObserverList;  // 观察者列表
    typedef std::map<int, ObserverList> ObserverMap; // (消息类型, 观察者), 用于观察者订阅关注的特定消息 
    ObserverMap observers_;
};

主题能为多个不同的消息类型注册并发出通知,这样观察者只需要订阅关心的特定消息即可。比如,对于一个表示一堆元素的主题,当添加或删除元素时,观察者可以选择订阅关注的“添加或删除元素”消息。

定义一个具体通知者类

#include "observer.h"

class MySubject : public ISubject // 具体的主题类
{
public:
    enum Message {ADD, REMOVE};   // 消息类型
    void Subsribe(int message, IObserver *observer) // 订阅消息
    {
        if (observer) {
            auto it = observers_[message];
            if (it == observers_.end()) {
                ObserverList list;
                list.push_back(observer);
                observers_[message] = list;
            } else {
               it->second.push_back(observer);
            }
        }
    }

    void Unsubsribe(int message, IObserver *observer) // 取消订阅的消息
    {
        auto it = observers_.find(message);
        if (it != observers_.end()) {
            it->second.remove_if(it->second.begin(), it->second.end(), [&observer](const IObserver* obj){ return obj == observer; });
        }
    }

    void Notify(int message) // 通知订阅消息的所有观察者
    {
        auto it = observers_.find(message);
        if (it != observers_.end() && !it->second.empty()) {
            for (auto obj : it->second) {
                if (obj) {
                    obj->Update(message);
                }
            }
        }
    }
};

通过继承IObserver抽象基类,实现Update()来创建观察者对象

#include "subject.h"
#include <iostream>
#include <string>

class MyObserver : public IObserver
{
public:
    explicit MyObserver(const std::string &str) : name_(str)
    {}

    void Update(int message)
    {
        std::cout << name_ << " Received message";
        std::cout << message << std::endl;
    }

private:
    std::string name_;
};

客户端

int main()
{
    // 构造3个观察者对象
    MyObserver observer1("observer1");
    MyObserver observer2("observer2");
    MyObserver observer3("observer3");
    // 构造1个主题对象
    MySubject subject;
    
    // 为观察者对象订阅关注的特定事件
    subject.Subscribe(MySubject::ADD, &observer1);
    subject.Subscribe(MySubject::ADD, &observer2);
    subject.Subscribe(MySubject::REMOVE, &observer2);
    subject.Subscribe(MySubject::REMOVE, &observer3);
    
    // 通知订阅特定事件的观察者
    subject.Notify(MySubject::ADD);
    subject.Notify(MySubject::REMOVE);
    return 0;
};

观察者模式对优势:主题(Subject)无需耦合某个具体的观察者(如MyObserver),而只需要知道其抽象接口IObserver即可。观察者对象需要事先在具体的主题(MySubject)中订阅关注的事件,当主题自身状态发生改变时,可Notify通知订阅了特定事件的所有观察者。整个过程中,MySubject跟MyObserver没有编译时依赖,也没有耦合。

观察者模式对缺点:性能损耗,即在函数调用前遍历观察者列表的开销。另外,在销毁观察者对象前,必须取消订阅此观察者对象,否则通知一个已销毁的观察者可能导致程序崩溃。


MVC架构

桌面程序和web程序中,有一个常用的模型就是MVC架构模式,该模式中,业务逻辑(model,模型)和用户界面(view,视图)分离,控制器(controller)接收用户输入并协调另外两者以确保同步。

MVC有以下优点:
1)model和view组件的隔离,可以方便实现多个用户界面,而且这些界面能重用公共的业务逻辑核心。
2)避免因多份UI实现而构建多份重复的底层模型代码的问题。-- 底层模型不再与View 1:1绑定,可复用
3)modle和view代码的解耦简化了单元测试;-- 功能单一了,更容易编写单元测试用例
4)组件的模块化允许核心逻辑开发者和GUI开发者并行工作,且互不影响。

核心:MVC模式促使核心业务逻辑(or 模型)与用户界面(or 视图)分离。隔离了控制器逻辑,控制器逻辑会引起模型的改变并更新视图。

下图展示了MVC模式对依赖关系:

视图代码能调用模型代码:发现最最新状态并更新UI。但模型代码不能调用视图代码,因为这样会导致模型绑定在一个视图上。

MVC本质上也是一种观察者模式。当模型状态改变时,控制器将这些改变传递给视图以更新UI。当然,在真实APP中,通常需要通过更新视图来反应底层模型的其他改变。


参考

[1]Martin Reddy, 刘晓娜, 臧秀涛,等. C++ API设计[M]. 人民邮电出版社, 2013.

posted @ 2022-07-04 17:01  明明1109  阅读(1178)  评论(0编辑  收藏  举报