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.