👂 Honey Honey - 孙燕姿 - 单曲 - 网易云音乐
目录
🌼前言
🌼描述
🎂问题
💪解决方案
🈲现实场景 + 代码
场景1 -- 报纸发行
场景
解释
代码
场景2 -- 气象资料发布
场景3 -- 过红绿灯
🏔观察者 -- 模式结构
🏦观察者 -- 适用情况
🐟实现方式
😖优缺点
🔚与其他模式的联系
🌼前言
《Linux多线程服务器端编程 使用muduo C++网络库》第 3 页提到,“本书默认大家已熟知 --观察者模式”,特地来学习
🌼描述
- 观察者模式是一种行为设计模式,允许你定义一种订阅机制,可在对象事件发生时,通知多个 “观察” 该对象的其他对象
- 定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新
- 一种行为型的设计模式,QT 中的信号槽就是一种典型的观察者模式
🎂问题
比如你有两种类型的对象:顾客 和 商店。
顾客对某个特定品牌的产品非常感兴趣(比如最新型号的 iphone 手机),该产品很快会在商店里出售。
顾客可以每天到商店里看看 iphone 到货了没。如果最新款 iphone 没到货,大多数到达商店的顾客就会空手而归(浪费资源)
还有个问题是:为了避免 “顾客每天到商店查看到货情况”,商店可以在每次新产品到货时,向 所有顾客 发送邮件,那么部分客户就不用反复前往商店。
但是这会打扰对新产品不感兴趣的顾客。
如何解决呢?
💪解决方案
观察者模式 就此登场:
发布者(publisher):自身状态的改变,通知到其他对象。
订阅者(subscribers):希望关注发布者状态变化的对象。
观察者模式中,发布者类拥有自己的 添加订阅机制,每个对象都能订阅或取消订阅发布者事件流
该机制包括:
1)一个用于存储订阅者对象的列表 2)几个用于添加或删除该列表中订阅者的方法
发布者发布通知后,都需要遍历订阅者列表,然后调用每个订阅者对象特定的通知方法
为了解耦订阅者和发布者,所有订阅者都要实现相同接口,发布者只通过接口和订阅者交互
如果有多种不同类型的发布者,而且要求订阅者要兼容所有发布者,那么就让所有发布者拥有相同的接口。
这个接口只需描述几个订阅方法
如此一来,订阅者就可以在不与具体发布者耦合的情况下,通过接口观察发布者的状态
🈲现实场景 + 代码
每个代码都是相似的,不同场景,增加熟练度
场景1 -- 报纸发行
场景
发布者 -- 出版社
订阅者 -- 渣渣辉
比方说,你订阅了一份报纸,那么就不需要再去报摊查询新出版的报纸了。
出版社(发布者)会在报纸出版后,直接将最新一期报纸寄到你的邮箱。
出版社(发布者)负责维护订阅者列表( subscriber() ),了解订阅者对哪些报纸感兴趣。
当订阅者不感兴趣后,他们随时可以从该列表退出。
解释
解释1 -- 代码中的接口
ISubscriber
和IPublisher
是两个接口,定义了订阅者和出版者的基本行为。这两个接口是基类。
ISubscriber
接口定义了一个Update
方法,这是具体订阅者需要实现的方法,用于接收来自出版者的消息。
IPublisher
接口定义了Attach
、Detach
和Notify
方法,分别用于添加订阅者、移除订阅者和通知所有订阅者。
Publisher
是IPublisher
的子类,实现了IPublisher
接口的所有方法。它维护了一个订阅者列表,并在有新消息时通知所有订阅者。
Subscriber
是ISubscriber
的子类,实现了ISubscriber
接口的Update
方法。它保存了一个出版者的引用,并可以从出版者的订阅者列表中添加或移除自己。在
ClientCode
函数中,创建了一个Publisher
对象和多个Subscriber
对象,并通过Attach
和Detach
方法管理订阅者列表。当Publisher
有新消息时,所有在订阅者列表中的Subscriber
都会收到通知。总的来说,
ISubscriber
和IPublisher
是基类,定义了一组行为Subscriber
和Publisher
是子类,实现了这些行为。这是典型的面向对象设计,通过接口和实现分离,使得代码更加灵活和可扩展
解释2 -- 整体逻辑
这段代码实现了观察者模式,
其中包含两个接口:ISubscriber(订阅者)和IPublisher(发布者)
具体的订阅者类Subscriber和发布者类Publisher分别实现了这两个接口
ISubscriber接口定义了一个Update方法,接收发布者的消息
IPublisher接口定义了Attach、Detach和Notify方法(添加一个订阅者、删除一个订阅者,通知所有订阅者)
Publisher类中的订阅者列表 list_subscriber_,存储所有订阅者
有新消息时,Publisher 通过 Notify 方法通知所有订阅者
Publisher 的 CreateMessage 方法,创建新消息 + 通知所有订阅者
Subscriber 类在构造函数中将自己添加到 Publisher 的订阅者列表,Update方法接收来自Publisher的消息
Subscriber 的 RemoveMeFromTheList(),从Publisher的订阅者列表中删除自己
main函数中,创建了一个Publisher对象和三个Subscriber对象
Publisher发布了几条消息,Subscriber接收并打印出这些消息
解释3 -- Publisher 接口
1)为什么 Subscriber 类需要通过通过统一接口 ISubscriber 实现呢?
是为了使 Publisher类 和 Subscriber类 解耦,具体的说👇
IPublisher接口 只依赖于 ISubscriber 接口,而不是具体的 Subscriber类,所以我们可以在不修改 Publisher类 的情况下,添加新的 Subsriber 类型,只要这个新的 Subscriber 类型实现了 ISubscriber 接口即可(这就是“解耦”)
2)那么为什么 Publisher 类也需要接口 IPublisher 呢?
比如说,你以后想要创建一个新的 Publisher 类,只需要实现 IPublisher 接口即可
任何依赖 IPublisher 接口的代码,都可以无缝衔接地使用这个新的 Publisher 类
解释4 -- 类的关系
参考博客👇
UML类图六种关系总结 - 知乎 (zhihu.com)
uml 类图依赖与关联的区别 - 掸尘 - 博客园 (cnblogs.com)
- Publisher 和 ISubscriber 之间是聚合关系
这就像一个报社(Publisher)和它的订阅者(ISubscriber)之间的关系
报社是整体,订阅者是部分
报社可以有很多订阅者,但是即使没有订阅者,报社依然可以存在
这就是所谓的"has-a"关系。
Subscriber 实现了 ISubscriber 接口,Publisher 实现了 IPublisher 接口
这就像一个具体的订阅者(Subscriber)是订阅者接口(ISubscriber)的具体实现,一个具体的报社(Publisher)是报社接口(IPublisher)的具体实现
这是一种"is-a"关系
main函数(也就是客户端代码)关联了 Publisher,依赖了 Subscriber
这就像客户端代码知道报社(Publisher)的存在,并且使用了订阅者(Subscriber)
这是一种"knows-a"关系和"uses-a"关系
客户端代码知道报社的存在,可以通过报社发布消息
同时,客户端代码也使用了订阅者,可以创建订阅者,让订阅者订阅报社的消息
代码
#include <iostream> // 输入输出流库
#include <list>
#include <string>// 订阅者接口
class ISubscriber {
public:virtual ~ISubscriber(){}; // 虚析构函数// 具体订阅者的更新方法;= 0 纯虚函数,子类必须自己实现virtual void Update(const std::string &message_from_publisher) = 0;
};// 出版社接口
class IPublisher {
public:// 纯虚函数:基类不能被实例化,只能在子类实现// 虚析构 -- 保证子类析构时能够调用基类析构virtual ~IPublisher(){};// 添加订阅者;= 0 子类自己实现virtual void Attach(ISubscriber *subscriber) = 0;// 移除订阅者;= 0 子类自己实现virtual void Detach(ISubscriber *subscriber) = 0;// 通知所有订阅者;= 0 子类自己实现virtual void Notify() = 0;
};// 具体出版社类
class Publisher : public IPublisher {
public:virtual ~Publisher() { // 析构std::cout << std::endl;std::cout << "报社要倒闭了!!!" << std::endl;}// override 表示子类覆盖父类的方法 -- 保证方法名和参数一致// 添加 -- 子类实现void Attach(ISubscriber *subscriber) override {list_subscriber_.push_back(subscriber);}// 删除void Detach(ISubscriber *subscriber) override {list_subscriber_.remove(subscriber);}// 通知void Notify() override {std::cout << std::endl;// 订阅者列表迭代器std::list<ISubscriber *>::iterator iterator = list_subscriber_.begin();HowManySubscriber(); // 输出订阅者数量while (iterator != list_subscriber_.end()) {// 更新所有订阅者(*iterator)->Update(message_);++iterator;}}// 创建消息 && 通知订阅者void CreateMessage(std::string message = "Empty") {// Empty 默认值,会被输入值覆盖this->message_ = message;Notify();}// 输出订阅者数量void HowManySubscriber() {std::cout << "有 " << list_subscriber_.size() << " 个傻逼看我的报纸" << std::endl;}// 其他业务逻辑,大事件发生时通知订阅者void SomeBusinessLogic() {this->message_ = "大事件!大事件!"; // 更改消息Notify(); // 通知std::cout << "房价跌到 300 块一平,快点入手!" << std::endl;}private:std::list<ISubscriber *> list_subscriber_; // 订阅者列表std::string message_; // 发送给订阅者的消息
};// 具体的订阅者类
class Subscriber : public ISubscriber {
public:// 构造函数Subscriber(Publisher &publisher) : publisher_(publisher) {this->publisher_.Attach(this); // 添加订阅者std::cout << std::endl;std::cout << "O(∩_∩)O渣渣 "<< ++Subscriber::static_number_ << " 号订阅了报纸" << std::endl;this->number_ = Subscriber::static_number_; // 订阅者编号}virtual ~Subscriber() {std::cout << std::endl;std::cout << "垃圾报纸,毁我青春,注销 " << this->number_ << " 号" << std::endl;}// 用新消息更新订阅者void Update(const std::string &message_from_publisher) override {message_from_publisher_ = message_from_publisher;PrintInfo();}// 从报社的订阅者列表,删除此订阅者void RemoveMeFromTheList() {publisher_.Detach(this);std::cout << std::endl;std::cout << "订阅者 " << number_ << " 不看报纸了" << std::endl;}// 打印新消息void PrintInfo() {std::cout << "订阅者 " << this->number_ << " 号: " << message_from_publisher_ << std::endl;}private:std::string message_from_publisher_; // 来自报社的信息Publisher &publisher_; // 报社的引用static int static_number_; // 订阅者实例的数量int number_; // 订阅者编号/*static 静态成员变量,只有一份拷贝所有实例共享通过类名::静态成员变量名访问类外初始化*/
};// static成员变量,类外初始化
int Subscriber::static_number_ = 0;// 客户端代码
int main() {Publisher *publisher = new Publisher; // 创建报社// 创建 3 个看报纸的人Subscriber *subscriber1 = new Subscriber(*publisher);Subscriber *subscriber2 = new Subscriber(*publisher);Subscriber *subscriber3 = new Subscriber(*publisher);// 报社发布消息 为空publisher->CreateMessage();// 报社发布新消息publisher->CreateMessage("房价又涨了!");// 第 2 个人退出subscriber2->RemoveMeFromTheList(); // 第 4 个人加入Subscriber *subscriber4 = new Subscriber(*publisher);// 发布新消息publisher->CreateMessage("我买了新车!!");// 发布新消息publisher->SomeBusinessLogic(); delete subscriber1;delete subscriber2;delete subscriber3;delete subscriber4;delete publisher; // 报社倒闭return 0;
}
O(∩_∩)O渣渣 1 号订阅了报纸O(∩_∩)O渣渣 2 号订阅了报纸O(∩_∩)O渣渣 3 号订阅了报纸有 3 个傻逼看我的报纸
订阅者 1 号: Empty
订阅者 2 号: Empty
订阅者 3 号: Empty有 3 个傻逼看我的报纸
订阅者 1 号: 房价又涨了!
订阅者 2 号: 房价又涨了!
订阅者 3 号: 房价又涨了!订阅者 2 不看报纸了O(∩_∩)O渣渣 4 号订阅了报纸有 3 个傻逼看我的报纸
订阅者 1 号: 我买了新车!!
订阅者 3 号: 我买了新车!!
订阅者 4 号: 我买了新车!!有 3 个傻逼看我的报纸
订阅者 1 号: 大事件!大事件!
订阅者 3 号: 大事件!大事件!
订阅者 4 号: 大事件!大事件!
房价跌到 300 块一平,快点入手!垃圾报纸,毁我青春,注销 1 号垃圾报纸,毁我青春,注销 2 号垃圾报纸,毁我青春,注销 3 号垃圾报纸,毁我青春,注销 4 号报社要倒闭了!!!
场景2 -- 气象资料发布
IDisplayA
和 IDisplayB
类是订阅者,它们实现了 IDisplay
接口并订阅了 DataCenter
发布的通知。DataCenter
类是发布者,它维护了一个订阅者列表,并在数据变化时通知所有的订阅者
气象站发布气象资料给数据中心,数据中心经过处理,将气象信息更新到两个不同的显示终端(A 和B)
上层:气象站
下层:显示中断
依赖:数据中心
代码2
#include <iostream>
#include <vector>
using namespace std;// IDisplay是一个抽象基类,定义了观察者的接口
class IDisplay {
public:virtual void show(float temperature) = 0; // 所有的观察者都需要实现这个方法virtual ~IDisplay() = default; // 虚析构函数,用于删除派生类的对象
};// IDisplayA是一个观察者,实现了IDisplay的接口
class IDisplayA : public IDisplay {
public:void show(float temperature) override { // 打印温度信息cout << "Display A: " << temperature << endl;}
};// IDisplayB也是一个观察者,实现了IDisplay的接口
class IDisplayB : public IDisplay {
public:void show(float temperature) override { // 打印温度信息cout << "Display B: " << temperature << endl;}
};// WeatherData是一个数据类,用于存储天气数据
class WeatherData {// 这里可以添加一些属性和方法,例如温度、湿度等
};// DataCenter是主题,它维护了一个观察者列表,并在状态改变时通知所有的观察者
class DataCenter {
public:void Attach(IDisplay *ob) { // 添加一个观察者obs.push_back(ob);}void Detach(IDisplay *ob) { // 删除一个观察者obs.erase(remove(obs.begin(), obs.end(), ob), obs.end());}void Notify() { // 通知所有的观察者float temper = CalcTemperature();for (auto iter = obs.begin(); iter != obs.end(); iter++) {(*iter)->show(temper);}}private:WeatherData* GetWeatherData() { // 获取天气数据// 这里可以添加一些代码,例如从数据库或者API获取天气数据return new WeatherData;}float CalcTemperature() { // 计算温度WeatherData *data = GetWeatherData();float temper = 25.0f; // 这里只是一个示例,实际的温度应该从WeatherData中获取delete data;return temper;}vector<IDisplay*> obs; // 观察者列表
};int main() {DataCenter *center = new DataCenter; // 创建一个DataCenter对象IDisplay *da = new IDisplayA; // 创建一个IDisplayA对象IDisplay *db = new IDisplayB; // 创建一个IDisplayB对象center->Attach(da); // 将IDisplayA添加到观察者列表center->Attach(db); // 将IDisplayB添加到观察者列表center->Notify(); // 通知所有的观察者delete da; // 删除IDisplayA对象delete db; // 删除IDisplayB对象delete center; // 删除DataCenter对象return 0;
}
场景3 -- 过红绿灯
发布者 -- 红绿灯
订阅者 -- 汽车
十字路口汽车等待红绿灯变化
代码3
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;// Observer class, which is an abstract class
// that other classes can inherit from to become observers
// 订阅者接口 Observer
class Observer {
public:Observer(int num):m_number(num){}// Virtual destructor to ensure correct deletion // when deleting derived classes through a base class pointervirtual ~Observer() {}// Pure virtual function to be implemented by derived classesvirtual void update(bool flag) = 0;
protected:int m_number;
};// Subject class, which is an abstract class
// that other classes can inherit from to become subjects
// 发布者接口 Subject
class Subject {
public:virtual ~Subject() {}virtual void attach(Observer* observer) = 0;virtual void detach(Observer* observer) = 0;virtual void notify(bool flag) = 0;
protected:vector<Observer*> observers;
};// TrafficLight class, which is a concrete subject
// that observers can attach to
// 发布者具体实现
class TrafficLight : public Subject {
public:void attach(Observer* observer) override {observers.push_back(observer);}void detach(Observer* observer) override {observers.erase(remove(observers.begin(), observers.end(), observer), observers.end());}// Notify all attached observers with the current statevoid notify(bool flag) override {if (flag) {cout << "Green light, go." << endl;} else {cout << "Red light, stop." << endl;}for (auto observer : observers) {observer->update(flag);}}
};// Car class, which is a concrete observer that can attach to a subject
// 订阅者具体实现
class Car : public Observer {
public:Car(int num) : Observer(num) {}// Update the state of the car based on the state of the traffic lightvoid update(bool flag) override {if (flag) {cout << "Car " << m_number << ": Start." << endl;} else {cout << "Car " << m_number << ": Stop." << endl;}}
};int main() {Subject *subject = new TrafficLight;Observer *car1 = new Car(1);Observer *car2 = new Car(2);// Attach the cars to the traffic lightsubject->attach(car1);subject->attach(car2);// Notify the cars with the state of the traffic lightsubject->notify(true);subject->notify(false);/*一个类有虚函数,那么它应该有一个虚析构函数这样,当删除一个指向派生类对象的基类指针时,派生类的析构函数也会被调用,防止资源泄露*/delete car1;delete car2;delete subject;return 0;
}
Green light, go.
Car 1: Start.
Car 2: Start.
Red light, stop.
Car 1: Stop.
Car 2: Stop.
🏔观察者 -- 模式结构
结合场景1代码,看👇的模式结构图
解释 -- 类图
- subscriber: Subscriber[]
// 私有 成员变量名: 类型
// subscriber 是类中的一个私有成员变量
// 这个变量的类型是:存储多个 Subscriber 对象的数组
+ subscribe(s: Subscriber)
// 公有 函数名(参数名: 参数类型)
- 发布者(Publisher)向其他对象发送信息。Notify() 会在报社自身状态改变 或 执行特定函数后发生。Publisher 类中包含 Detach() 和 Attach(),允许订阅者离开或加入。
- 新事件发生时,报社会遍历订阅列表 list_subscriber_,并调用每个订阅者的 Notify(),该方法在 ISubscriber(订阅者接口)声明。
- 订阅者接口(ISubscriber):大多数情况,该接口只包含一个 Update() 方法,该方法拥有多个参数来传递信息。
- 具体订阅者(Subscriber):执行一些操作回应发布者的通知。所有具体订阅者都实现了同样的接口,因此发布者不需要与具体的类耦合。
- 订阅者需要一些上下文信息来正确的更新。所以,发布者要将一些上下文数据,作为 Notify() 的参数传递给订阅者。发布者也可将自身作为参数传递,以便订阅者直接获取数据。
- 客户端(Client):分别创建发布者和订阅者,然后为订阅者注册发布者的更新。
🏦观察者 -- 适用情况
1
- 当一个对象状态的改变,需要改变其他对象;或实际对象是事先未知的或动态变化时,可以用观察者模式
- 比如说,使用图形界面时,用户创建了自定义按钮类,并允许客户端在按钮中注入自定义代码,这样当用户按下按钮,就会触发这些代码
观察者模式允许任何实现了订阅者接口的对象,订阅发布者对象的事件通知
你可以在按钮中添加订阅机制,允许客户端通过自定义订阅类注入自定义代码
2
当应用中的一些对象必须观察其他对象时,可使用观察者模式
但只能在有限时间或特定情况下使用
订阅列表是动态的,因此订阅者可以随时加入或离开该列表
🐟实现方式
- 根据业务逻辑,将 观察者模式 拆分为两部分:
a. 独立于其他代码的核心功能(发布者)
b. 其他代码(一组订阅者类)- 声明订阅者接口:该接口至少应声明一个 update() 方法
- 声明发布者接口:并定义一些接口用于在列表中添加 / 删除订阅对象(注意!发布者必须且只能通过订阅者接口和订阅对象进行交互)
- 确定存放实际订阅列表的位置,并实现订阅方法:
a. 因为所有类型的发布者都一样,那么,列表应该放置在直接扩展自发布者接口(IPublisher)的具体发布者(Publisher)中。具体发布者会扩展 接口类,从而继承所有的订阅行为
b. 但是,如果你需要在现有的类层次结构中应用观察者模式,最好使用组合的方式:将订阅逻辑放入一个独立的对象,然后让所有实际订阅者使用该对象- 创建具体发布者类(Publisher):每次发生重要事件,发布者都要通过所有订阅者
- 具体订阅者类(Subscriber)中实现通知更新的方法:
a. 订阅者需要一些与事件相关的上下文数据,这些数据(message_from_publisher)作为通知方法的参数来传递
b. 另一种选择:订阅者从通知中获取 所有数据,此时发布者通过更新方法,将自身传递出去
c. 还有一种不太灵活的方法:通过构造函数,将发布者与订阅者永久联系起来- 客户端生成所需的全部订阅者,并到相应发布者完成注册工作
😖优缺点
优点
1,开闭原则
你不用修改任何发布者代码就能引用新的订阅者类(良好的扩展性)
(如果是发布者接口,就能轻松引入发布者类)
2,可以在运行时建立对象之间的联系(稳定的信息更新传递机制)
3,耦合双方依赖于抽象,不需要了解具体
缺点
1,订阅者(也称观察者)的通知顺序是随机的(所以在设计观察者模式时,不要依赖于订阅者的通知顺序,这回让代码变得脆弱)
a. 订阅者运行时动态注册,那么注册顺序可能会变
b. 部分订阅者的注销,会影响剩下订阅者的通知顺序
c. 发布者可以以任何顺序通知定于这
2,某个订阅者出现卡顿,可能会影响整个进程,一般采用异步机制处理,同时注意线程安全
3,订阅者过多时,挨个通知每个订阅者,耗时较长
🔚与其他模式的联系
责任链模式,命令模式,中介者模式,观察者模式 ---- 是用于处理请求发送者和接收者间的不同连接方式
- 责任链:按照顺序将请求动态传递给一系列的潜在接收者,直至其中一名接收者对请求进行处理
- 命令:在发送者和接收者之间建立单向连接
- 中介者:清除发送者和接收者之间的直接连接,强制它们通过一个中介对象进行间接沟通
- 观察者:允许接收者动态的订阅或取消接收要求
中介者 和 观察者的区别往往很难记住,具体的:
- 中介者的主要目标是,消除一系列系统组间之间的相互依赖。这些组间依赖于同一个中介者对象
- 观察者的目标是,在对象间建立动态的单向连接,使得部分对象也可作为其他对象的附属发挥作用
- 有一种流行的中介者模式的实现,依赖于观察者模式:
a. 中介者对象担当发布者的角色,其他组件作为订阅者,可以订阅中介者的事件或取消订阅
b. 当中介者用这种方式实现时,它看起来和观察者一样- 你可能会困惑,此时可以采用其他方式来实现中介者。比如,永久的将所有组件链接到同一个中介者对象。这种实现方式和观察者长的很像,但它仍然是中介者模式
- 假设有一个程序,它所有的组件都变成了发布者,它们之间可以相互建立动态连接。这样程序就没有了中心化的中介者对象,只有一些分布式的观察者
参考文章👇
C++ 观察者模式讲解和代码示例 (refactoringguru.cn)
InterviewGuide大厂面试真题
设计模式之观察者模式(C++)-阿里云开发者社区 (aliyun.com)
观察者模式 (datawhalechina.github.io)
在 IDE 中使用 GitHub Copilot Chat - GitHub 文档