观察者模式 C++

👂 Honey Honey - 孙燕姿 - 单曲 - 网易云音乐

目录

🌼前言

🌼描述

🎂问题

💪解决方案

🈲现实场景 + 代码

场景1 -- 报纸发行

场景

解释

代码

场景2 -- 气象资料发布

场景3 -- 过红绿灯

🏔观察者 -- 模式结构

🏦观察者 -- 适用情况

🐟实现方式

😖优缺点

🔚与其他模式的联系


🌼前言

《Linux多线程服务器端编程 使用muduo C++网络库》第 3 页提到,“本书默认大家已熟知 --观察者模式”,特地来学习

🌼描述

  • 观察者模式是一种行为设计模式,允许你定义一种订阅机制,可在对象事件发生时,通知多个 “观察” 该对象的其他对象

  • 定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新
  • 一种行为型的设计模式,QT 中的信号槽就是一种典型的观察者模式

🎂问题

比如你有两种类型的对象:顾客 和 商店。

顾客对某个特定品牌的产品非常感兴趣(比如最新型号的 iphone 手机),该产品很快会在商店里出售。

顾客可以每天到商店里看看 iphone 到货了没。如果最新款 iphone 没到货,大多数到达商店的顾客就会空手而归(浪费资源)

还有个问题是:为了避免 “顾客每天到商店查看到货情况”,商店可以在每次新产品到货时,向 所有顾客 发送邮件,那么部分客户就不用反复前往商店。

但是这会打扰对新产品不感兴趣的顾客

如何解决呢?

💪解决方案

观察者模式 就此登场:

发布者(publisher):自身状态的改变,通知到其他对象。

订阅者(subscribers):希望关注发布者状态变化的对象。

观察者模式中,发布者类拥有自己的  添加订阅机制,每个对象都能订阅或取消订阅发布者事件流

该机制包括:

1)一个用于存储订阅者对象的列表    2)几个用于添加或删除该列表中订阅者的方法

发布者发布通知后,都需要遍历订阅者列表,然后调用每个订阅者对象特定的通知方法

为了解耦订阅者和发布者,所有订阅者都要实现相同接口,发布者只通过接口和订阅者交互

如果有多种不同类型的发布者,而且要求订阅者要兼容所有发布者,那么就让所有发布者拥有相同的接口。

这个接口只需描述几个订阅方法

如此一来,订阅者就可以在不与具体发布者耦合的情况下,通过接口观察发布者的状态

🈲现实场景 + 代码

每个代码都是相似的,不同场景,增加熟练度 

场景1 -- 报纸发行

场景

发布者 -- 出版社

订阅者 -- 渣渣辉

比方说,你订阅了一份报纸,那么就不需要再去报摊查询新出版的报纸了。

出版社(发布者)会在报纸出版后,直接将最新一期报纸寄到你的邮箱。

出版社(发布者)负责维护订阅者列表( subscriber() ),了解订阅者对哪些报纸感兴趣。

当订阅者不感兴趣后,他们随时可以从该列表退出。

解释

解释1 -- 代码中的接口

  1. ISubscriber 和 IPublisher 是两个接口,定义了订阅者和出版者的基本行为。这两个接口是基类。

  2. ISubscriber 接口定义了一个 Update 方法,这是具体订阅者需要实现的方法,用于接收来自出版者的消息。

  3. IPublisher 接口定义了 AttachDetach 和 Notify 方法,分别用于添加订阅者、移除订阅者和通知所有订阅者。

  4. Publisher 是 IPublisher 的子类,实现了 IPublisher 接口的所有方法。它维护了一个订阅者列表,并在有新消息时通知所有订阅者。

  5. Subscriber 是 ISubscriber 的子类,实现了 ISubscriber 接口的 Update 方法。它保存了一个出版者的引用,并可以从出版者的订阅者列表中添加或移除自己。

  6. 在 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 pointer
    virtual ~Observer() {}

    // Pure virtual function to be implemented by derived classes
    virtual 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 state
    void 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 light
    void 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 light
    subject->attach(car1);
    subject->attach(car2);

    // Notify the cars with the state of the traffic light
    subject->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)
// 公有 函数名(参数名: 参数类型)

  1. 发布者(Publisher)向其他对象发送信息。Notify() 会在报社自身状态改变 或 执行特定函数后发生。Publisher 类中包含 Detach() 和 Attach(),允许订阅者离开或加入。
  2. 新事件发生时,报社会遍历订阅列表 list_subscriber_,并调用每个订阅者的 Notify(),该方法在 ISubscriber(订阅者接口)声明。
  3. 订阅者接口(ISubscriber):大多数情况,该接口只包含一个 Update() 方法,该方法拥有多个参数来传递信息。
  4. 具体订阅者(Subscriber):执行一些操作回应发布者的通知。所有具体订阅者都实现了同样的接口,因此发布者不需要与具体的类耦合。
  5. 订阅者需要一些上下文信息来正确的更新。所以,发布者要将一些上下文数据,作为 Notify() 的参数传递给订阅者。发布者也可将自身作为参数传递,以便订阅者直接获取数据。
  6. 客户端(Client):分别创建发布者和订阅者,然后为订阅者注册发布者的更新。

🏦观察者 -- 适用情况

  • 当一个对象状态的改变,需要改变其他对象;或实际对象是事先未知的或动态变化时,可以用观察者模式
  • 比如说,使用图形界面时,用户创建了自定义按钮类,并允许客户端在按钮中注入自定义代码,这样当用户按下按钮,就会触发这些代码

观察者模式允许任何实现了订阅者接口的对象,订阅发布者对象的事件通知

你可以在按钮中添加订阅机制,允许客户端通过自定义订阅类注入自定义代码

当应用中的一些对象必须观察其他对象时,可使用观察者模式

但只能在有限时间或特定情况下使用

订阅列表是动态的,因此订阅者可以随时加入或离开该列表

🐟实现方式

  1. 据业务逻辑,将 观察者模式 拆分为两部分:
    a. 独立于其他代码的核心功能(发布者)
    b. 其他代码(一组订阅者类)
  2. 声明订阅者接口:该接口至少应声明一个 update() 方法
  3. 声明发布者接口:并定义一些接口用于在列表中添加 / 删除订阅对象(注意!发布者必须且只能通过订阅者接口和订阅对象进行交互)
  4. 定存放实际订阅列表的位置,并实现订阅方法:
    a. 因为所有类型的发布者都一样,那么,列表应该放置在直接扩展自发布者接口(IPublisher)的具体发布者(Publisher)中。具体发布者会扩展 接口类,从而继承所有的订阅行为
    b. 但是,如果你需要在现有的类层次结构中应用观察者模式,最好使用组合的方式:将订阅逻辑放入一个独立的对象,然后让所有实际订阅者使用该对象
  5. 创建具体发布者类(Publisher):每次发生重要事件,发布者都要通过所有订阅者
  6. 体订阅者类(Subscriber)中实现通知更新的方法:
    a. 订阅者需要一些与事件相关的上下文数据,这些数据(message_from_publisher)作为通知方法的参数来传递
    b. 另一种选择:订阅者从通知中获取 所有数据,此时发布者通过更新方法,将自身传递出去
    c. 还有一种不太灵活的方法:通过构造函数,将发布者与订阅者永久联系起来
  7. 客户端生成所需的全部订阅者,并到相应发布者完成注册工作

😖优缺点

优点

1,开闭原则

你不用修改任何发布者代码就能引用新的订阅者类(良好的扩展性)

(如果是发布者接口,就能轻松引入发布者类)

2,可以在运行时建立对象之间的联系(稳定的信息更新传递机制)

3,耦合双方依赖于抽象,不需要了解具体

缺点

 1,订阅者(也称观察者)的通知顺序是随机的(所以在设计观察者模式时,不要依赖于订阅者的通知顺序,这回让代码变得脆弱)

a. 订阅者运行时动态注册,那么注册顺序可能会变

b. 部分订阅者的注销,会影响剩下订阅者的通知顺序

c. 发布者可以以任何顺序通知定于这

2,某个订阅者出现卡顿,可能会影响整个进程,一般采用异步机制处理,同时注意线程安全

3,订阅者过多时,挨个通知每个订阅者,耗时较长

🔚与其他模式的联系

责任链模式命令模式中介者模式观察者模式 ---- 是用于处理请求发送者和接收者间的不同连接方式

  • 责任链:按照顺序将请求动态传递给一系列的潜在接收者,直至其中一名接收者对请求进行处理
  • 命令:在发送者和接收者之间建立单向连接
  • 中介者:清除发送者和接收者之间的直接连接,强制它们通过一个中介对象进行间接沟通
  • 观察者:允许接收者动态的订阅或取消接收要求

中介者 和 观察者的区别往往很难记住,具体的:

  1. 中介者的主要目标是,消除一系列系统组间之间的相互依赖。这些组间依赖于同一个中介者对象
  2. 观察者的目标是,在对象间建立动态的单向连接,使得部分对象也可作为其他对象的附属发挥作用
  3. 有一种流行的中介者模式的实现,依赖于观察者模式:
    a. 中介者对象担当发布者的角色,其他组件作为订阅者,可以订阅中介者的事件或取消订阅
    b. 当中介者用这种方式实现时,它看起来和观察者一样
  4. 你可能会困惑,此时可以采用其他方式来实现中介者。比如,永久的将所有组件链接到同一个中介者对象。这种实现方式和观察者长的很像,但它仍然是中介者模式
  5. 假设有一个程序,它所有的组件都变成了发布者,它们之间可以相互建立动态连接。这样程序就没有了中心化的中介者对象,只有一些分布式的观察者

参考文章👇

C++ 观察者模式讲解和代码示例 (refactoringguru.cn)

InterviewGuide大厂面试真题

设计模式之观察者模式(C++)-阿里云开发者社区 (aliyun.com)

观察者模式 (datawhalechina.github.io)

在 IDE 中使用 GitHub Copilot Chat - GitHub 文档

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/519155.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

渗透测试:数据库UDF提权(linux)

目录 开头: 1.UDF提权简介&#xff1a; 1.1共享库文件(UDF文件)指定目录&#xff1a; 版本特征&#xff1a; 操作系统版本&#xff1a; 2.靶场UDF提权复现 提权前提 1.要有一个高权限的MySQL的账号 ​编辑 2.MySQL的权限配置secure_file_priv为空 3.必须有存放UDF文件的…

nginx详细配置,高可用

Nginx是一款高性能的Web服务器和反向代理服务器&#xff0c;通常用来进行负载均衡&#xff0c;提供高可用的服务。而Keepalived是一款开源的高可用性解决方案&#xff0c;可以提高系统的可靠性和稳定性。使用Nginx和Keepalived来配置高可用服务的具体步骤如下&#xff1a;1. 安…

题库管理系统-基于Springboot实现JAVA+SQL离散数学题库管理系统(完整源码+LW+翻译)

基于Springboot实现JAVASQL离散数学题库管理系统(完整源码LW翻译) 概述&#xff1a; 本系统具体完成的功能如下&#xff1a; 1题库的管理与维护&#xff1a;新题的录入&#xff0c;修改&#xff0c;删除等功能。 2生成试卷&#xff1a;包括自动生成与手工改动&#xff0c;要…

vue创建项目下载动态路由v-for mounted websocket :style :class store使用说明

在Vue中创建一个项目&#xff0c;并整合动态路由、v-for、mounted生命周期钩子、WebSocket、:style、:class以及Vuex的store&#xff0c;涉及到多个Vue核心特性的使用。下面我将简要说明如何逐步整合这些特性。 1. 创建Vue项目 使用Vue CLI创建项目&#xff1a; 2. 配置动态路…

react状态管理库---zustand

一个简单的&#xff0c;快速的状态管理解决方案&#xff0c;api设计基于函数式和hooks 安装&#xff1a; npm install zustand 基础使用 让我们实现一个非常简单的计数器案例完成我们的第一个store 1- 创建一个counterStore create( ) 有三个参数&#xff1a;函数、布尔值…

某音乐平台歌曲信息逆向之参数寻找

如何逆向加密参数&#xff1a;某音乐平台歌曲信息逆向之webpack扣取-CSDN博客 参数构建 {"comm": {"cv": 4747474,"ct": 24,"format": "json","inCharset": "utf-8","outCharset": "ut…

从0到1实现RPC | 04 负载均衡和静态注册中心

一、Router的定义 Router路由用于预筛选&#xff0c;Dubbo有这样的设计&#xff0c;SpringCloud没有。 二、LoadBanlancer定义 负载均衡器&#xff1a;默认取第一个 当前支持随机和轮询两种负载均衡器。 随机&#xff1a;从所有provider中随机选择一个。 轮询&#xff1a;每…

ctf奇淫巧计

%00截断&#xff1a; %00在urldecode后就是0x00&#xff0c;一些函数诸如preg_match遇到0x00会直接停止。 String tartget_url req.getParameter("url");String pri tartget_url.substring(0, tartget_url.indexOf(":"));System.out.println(pri);if (p…

使用Python转换图片中的颜色

说明&#xff1a;最近在看梵高的画册&#xff0c;我手上的这本画册&#xff08;《文森特梵高》杨建飞 主编&#xff09;书中说&#xff0c;梵高用的颜料里有不耐久的合成颜料&#xff0c;原本的紫色褪成了我们现在所看到的灰蓝色。于是我想&#xff0c;能不能用程序将画中的颜色…

备战蓝桥杯---贡献法刷题

话不多说&#xff0c;直接看题&#xff1a; 什么是贡献法&#xff1f;这是一种数学思想&#xff0c;就是看每一个元素对总和的贡献。 1. 我们可以先枚举区间再统计次数&#xff0c;但这显然TLE。我们可以发现&#xff0c;每一个孤独的区间对应一个孤独的牛&#xff0c;因此我…

计组第三版书例题

基础知识过一下 存储器与CPU的连接主要通过数据总线、地址总线和控制总线实现。CPU首先向存储器发送地址信号&#xff0c;然后发出读写控制信号&#xff0c;最后在数据总线上进行数据的读写操作 。这种连接方式确保了CPU能够正确地访问和控制存储器中的数据。 https://blog.cs…

2024免费Mac电脑用户的系统清理和优化软件CleanMyMac

作为产品营销专家&#xff0c;对于各类产品的特性与优势有着深入的了解。CleanMyMac是一款针对Mac电脑用户的系统清理和优化软件&#xff0c;旨在帮助用户轻松管理、优化和保护Mac电脑。以下是关于CleanMyMac的详细介绍&#xff1a; CleanMyMac X2024全新版下载如下: https://…

蓝桥-时间显示

目录 题目链接 代码 题目链接 1.时间显示 - 蓝桥云课 (lanqiao.cn) 代码 #include <bits/stdc.h> using namespace std;int main() {long long x;cin>>x;int h,m,s;x x / 1000 % (3600*24); // 毫秒化秒&#xff0c;并且保留最后一天的时间h x / 3600; //求得…

使用pip install替代conda install将packet下载到anaconda虚拟环境

问题描述 使用conda install 下载 stable_baseline3出现问题 一番搜索下是Anaconda.org缺少源 解决方法 首先使用管理员权限打开 anaconda prompt 然后激活目标环境&#xff1a;conda activate env_name 接着使用&#xff1a;conda env list查看目标env的位置 如D:\anacon…

C语言进阶课程学习记录-第23课 - #error 和 #line 使用分析

C语言进阶课程学习记录-第23课 - #error 和 #line 使用分析 实验-#errer的使用演示cmd窗口实验-缺少#error实验-#line 1的使用实验-#line 1用于标记代码小结 本文学习自狄泰软件学院 唐佐林老师的 C语言进阶课程&#xff0c;图片全部来源于课程PPT&#xff0c;仅用于个人学习记…

MySQL介绍和安装

MySQL介绍和安装 文章目录 MySQL介绍和安装1.MySQL介绍2.MySQL安装2.1 主机初始化2.1.1 设置网卡名和ip地址2.1.2 配置镜像源2.1.3 关闭防火墙2.1.4 禁用SELinux2.1.5 设置时区 2.2 包安装2.2.1 Rocky和CentOS 安装 MySQL2.2.2 Ubuntu 安装 MySQL 2.3 二进制安装安装MySQL2.3.1…

基于SpringBoot+微信小程序的防诈骗平台

一、项目背景介绍&#xff1a; 社会背景随着互联网的高速发展&#xff0c;网络和手机的普及率也大大提高&#xff0c;这也衍生出一系列问题&#xff1a;用户信息泄露、不法分子电话诈骗等…现越来越多的老年人甚至年轻人经历过电信诈骗并被骗了大量金额。该产品正是基于这样的社…

CMOS漏极开路门

线与 通常CMOS门电路都有反相器作为输出缓冲电路。在实际工程中&#xff0c;为了方便常将两个门的输入端直接并联来实现与逻辑功能&#xff08;称为线与&#xff09;。如下图所示&#xff1a; 线与的弊端&#xff1a;当与电源VDD直接相连的PMOS管导通时&#xff0c;由于MOS管导…

arm开发板移植工具mkfs.ext4

文章目录 一、前言二、手动安装e2fsprogs1、下载源码包2、解压源码3、配置4、编译5、安装 三、移植四、验证五、总结 一、前言 在buildroot菜单中&#xff0c;可以通过勾选e2fsprogs工具来安装mkfs.ext4工具&#xff1a; Target packages -> Filesystem and flash utilit…

发布自己的github项目

git下载 git关网&#xff1a;https://git-scm.com/ 下载后是exe文件 git安装 除了选安装地址&#xff0c;其他都是下一步下一步傻瓜式安装 安装好之后随便一个地方右键多了两个东西 git gui here 和git bash here git测试配置及创建github项目 右键git bash here 测试…