【C++设计模式】第三篇:观察者模式(别名:发布-订阅模式、模型-视图模式、源-监听器模式)
C++设计模式系列文章目录
【C++设计模式】第一篇 C++单例模式–懒汉与饿汉以及线程安全
【C++设计模式】第二篇:策略模式(Strategy)–从基本介绍,内部原理、应用场景、使用方法,常见问题和解决方案进行深度解析
【C++设计模式】第三篇:观察者模式(别名:发布-订阅模式、模型-视图模式、源-监听器模式)
- C++设计模式系列文章目录
- 一、观察者模式简介
- 二、解决的问题类型
- 三、使用场景
- 四、核心概念
- 五、实现方式
- 5.1 基础方式
- 5.2 改进观察者模式
- 5.3 总结
- 八、最终小结
- 一句话总结(发布-订阅模式):
原文链接:https://blog.csdn.net/weixin_44131922/article/details/149515925
一、观察者模式简介
观察者模式(Observer Pattern) 是一种 行为型设计模式(对象行为型模式),它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象发生变化时,它的所有依赖者(观察者)都会收到通知并自动更新。
你可以把它想象成“订阅-发布”机制:当你订阅了一份杂志后,每当有新一期出版时,你就会收到通知。这里的“你”就是观察者,“杂志”则是被观察的主题。
“红灯停,绿灯行”。在这个过程中,交通信号灯是汽车的观察目标,而汽车则是观察者。随着交通信号灯的变化,汽车的行为也随之变化,一盏交通信号灯可以指挥多辆汽车。
在软件系统中有些对象之间也存在类似交通信号灯和汽车之间的关系,一个对象的状态或行为的变化将导致其他对象的状态或行为也发生改变。观察者模式(Observer Pattern)则是用于建立一种对象与对象之间的依赖关系,使得每当一个对象状态发生改变时其相关依赖对象皆得到通知并被自动更新。发生改变的对象称为观察目标,被通知的对象称为观察者,一个观察目标可以对应多个观察者。它有如下别名:
- 发布-订阅(Publish/Subscribe)模式
- 模型-视图(Model/View)模式
- 源-监听器(Source/Listener)模式
- 从属者(Dependents)模式
二、解决的问题类型
观察者模式主要用于解决以下问题:
- 状态变化需要通知相关联的对象:例如,用户界面中的模型数据发生变化时,视图需要自动更新。需要在系统中创建一个触发链。
- 避免紧耦合:允许对象之间保持松散的联系,无需直接相互引用。一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用。
- 实现广播通信:可以向所有注册的观察者发送消息,而不需要知道它们的具体身份。
三、使用场景
四、核心概念
Subject(主题/被观察者):维护了一个观察者列表,并提供方法供观察者注册或移除自身;在状态改变时通知所有观察者。
Observer(观察者):接收来自主题的通知并作出响应。
ConcreteSubject & ConcreteObserver:具体实现上述两个接口的实际类。
五、实现方式
原文链接:https://blog.csdn.net/hzdxyh/article/details/141126923
- Observer(观察者):它是一个抽象类或接口,为所有的具体观察者定义一个更新接口,使得在得到主题的通知时更新自己。
- Subject(主题):它维护了一系列依赖于它的Observer对象,并提供一个接口来允许Observer对象注册自己、注销自己以及通知它们。
- ConcreteObserver(具体观察者):它实现了Observer接口,存储与Subject的状态自洽的状态。具体观察者根据需要实现Subject的更新接口,以使得自身状态与主题的状态保持一致。
- ConcreteSubject(具体主题):它实现了Subject接口,将有关状态存入具体观察者对象,并在状态发生改变时向Observer发出通知。
5.1 基础方式
按定义实现最基础的观察者模式功能:
/*** @brief 定义一个Observer抽象基类*/
class IObserver {public:IObserver() {}virtual ~IObserver() {}public:virtual void Update(int data) = 0;
};/*** @brief 定义一个Subject抽象基类*/
class ISubject {public:ISubject() {}virtual ~ISubject() {}public:virtual void Subscribe(std::shared_ptr<IObserver> observer) = 0; // 观察者订阅事件virtual void Unsubscribe(std::shared_ptr<IObserver> observer) = 0; // 观察者取消事件的订阅virtual void Notify(int data) = 0; // 通知已订阅指定事件的观察者public:std::list<std::weak_ptr<IObserver>> observers_; // 存放所有已订阅的observer
};/*** @brief 实现一个具体的观察者*/
class ConcreteObserver : public IObserver {public:ConcreteObserver(const std::string& name) : name_(name) {}virtual ~ConcreteObserver() override {}public:void Update(int data) override {std::cout << "observer [" << name_ << "] updated -> " << data << std::endl;}private:std::string name_;
};/*** @brief 实现一个具体的Subject*/
class ConcreteSubject : public ISubject {public:ConcreteSubject() {}virtual ~ConcreteSubject() override {}public:void Subscribe(std::shared_ptr<IObserver> observer) override {observers_.push_back(observer);};void Unsubscribe(std::shared_ptr<IObserver> observer) override {observers_.erase(std::remove_if(observers_.begin(), observers_.end(),[&observer](std::weak_ptr<IObserver> obj) {std::shared_ptr<IObserver> tmp =obj.lock();if (tmp != nullptr) {return tmp == observer;} else {return false;}}),observers_.end());}void Notify(int data) override {for (auto it = observers_.begin(); it != observers_.end(); ++it) {std::shared_ptr<IObserver> ps = it->lock();// weak_ptr提升为shared_ptr// 判断对象是否还存活if (ps != nullptr) {ps->Update(data);} else {it = observers_.erase(it);}}}
};// 测试
int main() {// 构造3个观察者对象std::shared_ptr<IObserver> observer1(new ConcreteObserver("observer1"));std::shared_ptr<IObserver> observer2(new ConcreteObserver("observer2"));std::shared_ptr<IObserver> observer3(new ConcreteObserver("observer3"));// 构造1个主题对象ConcreteSubject subject;// 为观察者订阅事件subject.Subscribe(observer1);subject.Subscribe(observer2);subject.Subscribe(observer3);// 通知订阅事件的观察者subject.Notify(10);// 模拟取消订阅subject.Unsubscribe(observer1);// 通知订阅事件的观察者subject.Notify(20);return 0;
}
控制台输出:
observer [observer1] updated -> 10
observer [observer2] updated -> 10
observer [observer3] updated -> 10
observer [observer2] updated -> 20
observer [observer3] updated -> 20
注意:在ISubject 中维护了一个已订阅的observer的list,在list中存放了observer对象的指针,在很多例子中list中直接存放observer的裸指针,在notify通知所有observer时,需要遍历list,调用每个observer的update接口,在多线程环境中,肯定不明确此时observer对象是否还存活,或是已经在其它线程中被析构了。本例这里使用了weak_ptr和shared_ptr替代observer裸指针,解决上述问题,同时还能避免循环引用
从上面的例子中可以看出:Observer是不依赖于Subject的,想要增加一个新的Observer只需要继承IObserver即可,无需修改Subject,这符合开闭原则,也实现了Observer与Subject的解耦。
5.2 改进观察者模式
上面例子中的观察者模式是经典模式,但是存在缺陷:
-
需要继承,继承是强对象关系,只能对特定的观察者才有效,即必须是Observer抽象类的派生类才行;
-
观察者被通知的接口参数不支持变化,导致观察者不能应付接口的变化
为了解决上例观察者模式的缺陷,可使用C++11 做出改进 -
通过被通知接口参数化和std::function 来代替继承;
-
通过可变参数模板和完美转发来消除接口变化产生的影响。
下面以一个具体的场景举例:
- 有一个数据中心,数据中心负责从其他地方获取数据,并对数据进行加工处理
- 有一个图表组件,需要从数据中心拿数据进行可视化展示
- 有一个文本组件,需要从数据中心拿数据进行可视化展示
- 后期可能还有更多的组件,需要从数据中心拿数据…
/*** @brief 实现一个具体的Subject,模拟一个数据中心*/
template <typename Func>
class Subject {public:static Subject& GetInstance() {static Subject instance;return instance;}public:// 注册观察者,右值引用int Subscribe(Func&& f) { return Assign(f); }// 注册观察者,左值int Subscribe(const Func& f) { return Assign(f); }// 移除观察者void Unsubscribe(int id) { observers_map_.erase(id); }// 通知所有观察者template <typename... Args>void Notify(Args&&... args) {for (auto& it : observers_map_) {auto& func = it.second;func(std::forward<Args>(args)...);}}private:template <typename F>int Assign(F&& f) {int id = observer_id_++;observers_map_.emplace(id, std::forward<F>(f));return id;}private:Subject() = default;~Subject() = default;Subject(const Subject&) = delete;Subject& operator=(const Subject&) = delete;Subject(Subject&&) = delete;Subject& operator=(Subject&&) = delete;int observer_id_ = 0; //观察者对应编号std::map<int, Func> observers_map_; // 观察者列表
};/*** @brief 实现一个具体的观察者,模拟图表展示组件*/
class ChartView {public:ChartView() {}~ChartView() {}public:void Update(int data) {std::cout << "the chart data has been updated to [" << data << "]" << std::endl;}
};/*** @brief 实现一个具体的观察者,模拟文本展示组件*/
class TextView {public:TextView() {}~TextView() {}public:void Update(std::string key, std::string value) {std::cout << "the text data has been updated to [" << key << ": " << value << "]" << std::endl;}
};// 测试程序
int main(void) {ChartView cv; // 图表展示组件// 从数据中心订阅Subject<std::function<void(int)>>::GetInstance().Subscribe(std::bind(&ChartView::Update, cv, std::placeholders::_1));TextView tv; // 文本展示组件// 从数据中心订阅Subject<std::function<void(std::string, std::string)>>::GetInstance().Subscribe(std::bind(&TextView::Update, tv, std::placeholders::_1, std::placeholders::_2));// 这里模拟一个数据处理中心线程std::thread([]() {int cnt = 0;while (1) {// 模拟数据获取数据处理过程std::this_thread::sleep_for(std::chrono::seconds(1));auto time = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();// 数据处理完成后,通知组件Subject<std::function<void(int)>>::GetInstance().Notify(cnt++);Subject<std::function<void(std::string, std::string)>>::GetInstance().Notify("time", std::to_string(time));}}).detach();// 主线程while (1) {std::cout << "main run ..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(5));}return 0;
}
main run ...
the chart data has been updated to [0]
the text data has been updated to [time: 1723446799]
the chart data has been updated to [1]
the text data has been updated to [time: 1723446800]
the chart data has been updated to [2]
the text data has been updated to [time: 1723446801]
the chart data has been updated to [3]
the text data has been updated to [time: 1723446802]
main run ...
the chart data has been updated to [4]
the text data has been updated to [time: 1723446803]
在本例中,将Subject改为单例模式,这样在组件中调用注册接口,在数据中心调用通知接口,完全解耦分离;图表组件和文本组件所需的数据个数和数据类型是不同的,这在基础的观察者模式中是无法实现的,改进后的观察者模式脱离了需要继承的约束,可以实现更加通用的功能,后期扩展更多组件时,不需要修改Subject代码,只需要新增Observer即可。
5.3 总结
观察者模式的优点主要包括:
解耦:观察者和被观察的对象是抽象耦合的,即它们之间不直接调用,而是通过消息传递来通知。
灵活性:可以在运行时动态地添加或删除观察者。
复用性:观察者模式可以单独地重用主题和观察者。
缺点
开销:如果观察者非常多,那么更新的效率就会比较低,因为需要遍历所有的观察者,并调用它们的更新方法。
八、最终小结
观察者模式是一种非常实用的设计模式,特别适合那些需要在不同组件之间维持松散耦合并且能够响应状态变化的应用场景。通过合理地运用观察者模式,我们可以在不破坏原有模块独立性的前提下,有效地实现组件间的协作与通信。
在开发图形用户界面(GUI)、实时数据监控系统、消息中间件等项目时,观察者模式能够帮助你更好地组织代码结构,提高系统的灵活性和可维护性。
一句话总结(发布-订阅模式):
观察者模式就像一个新闻通讯社,一旦有新的新闻发布,所有订阅了该新闻的人都会立即收到通知。