C++ 计划模式 十九:观察者模式 (读书 当代c++计划模式)

[复制链接]
发表于 2025-10-22 23:40:33 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
观察者



近期在学 GUI 编程(Qt), 以是本日跳读, 先把第19类计划模式(观察者模式)给看了.
观察者模式的焦点应用场景是 实现对象间的一对多依赖关系,当被观察对象(Subject)的状态厘革须要自动关照多个依赖对象(Observer)时利用。
相较于其他语言, c++ 标准库并没有给出现成实现, 实现一个安全的、正确的观察者(如果存在如许的东西的话)从技能上来说是比力复杂的.
属性观察者

如今是我们的需求: 当人长大一岁, 我们要为她庆祝生日, 为了实现, 我们先给出界说:
  1. struct Person {
  2.         int age_;
  3.         explicit Person(const int age) : age_{age} {}
  4. };
复制代码
但是很显然, 如今很难知道一个人的年事在何时厘革. 为了获取这个消息, 可以接纳轮询, 好比每100ms读一次年事, 然后和之前的年事比力.不外这方法显然扩展性不敷好, 我们接纳一些别的方法.
当一个改变年事字段的写利用发生时,我们想要捕获这一消息。捕获这个关照的的唯一方法是创建setter。
  1. struct Person {
  2. private:
  3.         int age_;
  4. public:
  5.         explicit Person(const int age) : age_{age} {}
  6.         ~Person() = default;
  7.         [[nodiscard]] int get_age() const {
  8.                 return age_;
  9.         }
  10.         void set_age(const int age) {
  11.                 age_ = age;
  12.         }
  13. };
复制代码
如今我们为年事创建了一个 setter 即 set_age(), 至于怎样实现关照, 我们须要一些其他的类.
观察者 Observer

第一种方式, 我们界说一个接口, 任何须要获取 Person 对象厘革信息的人都要给出实现, 我们将接口实现如下:
  1. struct PersonListener {
  2.         virtual void person_changed(Person& p, const string& property_name,
  3.                                     const int any_new_value) = 0;
  4. };
复制代码
不外这不敷通用, 如果对于每一种范例我们都去实现一个 Listener 着实贫困, 以是我们用上泛型.
  1. template <typename T>
  2. struct Observer {
  3.         virtual ~Observer() = default;
  4.         virtual void field_changed(T& source, const string& field_name) = 0;
  5. };
复制代码
这里的函数 field_changed() 就是我们的虚函数接口了, 此中的两个入参就和他们的名字一样, source 是对要更改字段的引用, field_name 就是要更改字段的名称, 此处是作为字符串传入的(不外如许做会陵犯代码重构性).
   作者注1:c#在一连的版本中两次明白地办理了这个标题。起首,它引入了一个名为CallerMemberName的属性,该属性将调用函数/属性的名称作为参数的字符串值插入。第二个版本简单地引入了nameof(Foo),它将取符号的名称并将其转换为字符串。
  好的, 如今如许实现之后, 我们就可以观察 Person 类的厘革了, 第一个利用方式: 将他们写到终端:
  1. struct ConsolePersonObserver final : public Observer<Person> {
  2.         void field_changed(Person& source, const string& field_name) override {
  3.                 cout << "Person's " << field_name << "has changed to " << source.get_age()
  4.                          << "." << endl;
  5.         }
  6. };
复制代码
通过继续差别范例的 Observer , 我们就能获取多个差别类的厘革, 比方引入一个 Creature 类:
  1. #include <iostream>#include <string>using namespace std;struct Person {
  2. private:
  3.         int age_;
  4. public:
  5.         explicit Person(const int age) : age_{age} {}
  6.         ~Person() = default;
  7.         [[nodiscard]] int get_age() const {
  8.                 return age_;
  9.         }
  10.         void set_age(const int age) {
  11.                 age_ = age;
  12.         }
  13. };
  14. struct Creature {private:        int attack_{0};        int defense_{0};public:        explicit Creature() {}        [[nodiscard]] int get_attack() const {                return attack_;        }        void set_attack(const int attack) {                attack_ = attack;        }        [[nodiscard]] int get_defense() const {                return defense_;        }        void set_defense(const int defense) {                defense_ = defense;        }};template <typename T>
  15. struct Observer {
  16.         virtual ~Observer() = default;
  17.         virtual void field_changed(T& source, const string& field_name) = 0;
  18. };
  19. struct ConsolePersonObserver final : public Observer<Person> {
  20.         void field_changed(Person& source, const string& field_name) override {
  21.                 cout << "Person's " << field_name << "has changed to " << source.get_age()
  22.                          << "." << endl;
  23.         }
  24. };
  25. struct ConsolePersonCreatureObserver final : Observer<Person>, Observer<Creature> {        void field_changed(Person &source, const string &field_name) override {}        void field_changed(Creature &source, const string &field_name) override {}};
复制代码

按照作者所说:
   另一种替换方法是利用std::any,并去掉泛型实现。试一试!
  这里给出一个实现(std::any要求标准在c++17及以上):
  1. #include <iostream>#include <string>#include <any>using namespace std;struct Person {
  2. private:
  3.         int age_;
  4. public:
  5.         explicit Person(const int age) : age_{age} {}
  6.         ~Person() = default;
  7.         [[nodiscard]] int get_age() const {
  8.                 return age_;
  9.         }
  10.         void set_age(const int age) {
  11.                 age_ = age;
  12.         }
  13. };
  14. struct Creature {private:        int attack_{0};        int defense_{0};public:        explicit Creature() {}        [[nodiscard]] int get_attack() const {                return attack_;        }        void set_attack(const int attack) {                attack_ = attack;        }        [[nodiscard]] int get_defense() const {                return defense_;        }        void set_defense(const int defense) {                defense_ = defense;        }};struct Observer {        virtual ~Observer() = default;        virtual void field_changed(any& source, const string& field_name) = 0;};struct ConsolePersonObserver final : public Observer {        void field_changed(any& source, const string& field_name) override {                if (const auto p = any_cast<Person*>(source)) {                        cout << "Person's " << field_name << "has changed to " << p->get_age()                                << "." << endl;                }        }};struct ConsolePersonCreatureObserver final : Observer {        void field_changed(any& source, const string& field_name) override {                if (auto p = any_cast<Person*>(source)) {                        // 处置惩罚Person逻辑                } else if (auto c = any_cast<Creature*>(source)) {                        // 处置惩罚Creature逻辑                }        }};
复制代码
总体思绪就是引入 std::any 之后可以去掉 Observer 的泛型依赖, 将范例判定从编译时(继续)转移到了运行时(具体的 Observer 内部做检测).
被观察者 Observable

如今来到了关键标题: 我们怎样让观察者和被观察者之间举行通讯?
我们如许想, 如果我们让观察者来实现消息获取机制, 那么对于每一个观察者来说都要实现一个获取函数, 同时我们不得不接纳轮询方式向被观察者发送哀求信息, 显然这不是个好计划.
那么我们倒置一下职责, 让被观察者来实现消息机制, 也就是被观察者一旦发生了厘革立马向观察者转达自身厘革了的信息.
在我们的例子里, 被观察者就是 Person 以是我们回到这个类, 对他做一些修改.


  • 为了提示全部观察者, 我们在被观察者内部要维护一个观察者聚集
  • 让观察者可以订阅大概取消订阅 subscribe()/unsubscribe() 发生在Person上的厘革。
  • 当Person发生改变的时间关照全部的观察者
为此我们界说一个被观察者要实现的接口:
  1. template <typename T>
  2. struct Observable {
  3. private:
  4.         std::vector<std::weak_ptr<Observer<T>>> observers_; // 存储弱引用
  5. public:
  6.         void subscribe(std::shared_ptr<Observer<T>> f) {
  7.                 observers_.emplace_back(f); // 自动转换为 weak_ptr
  8.         }
  9.         void unsubscribe(const std::shared_ptr<Observer<T>>& f);
  10.         void notify(T& source, const string& name);
  11. };
复制代码
起首看到了我们的类 Observable , 这就是我们的抽象被观察者


  • 这是一个模板类, 为了担当差别范例的 Observer 而计划.
  • 然后是数据成员, 我们用一个 vector 来维护全部的观察者, vector 内是差别 Observer 的指针, 由于被观察者通常不持有观察者的全部权, 以是我们用 weak_ptr 来管理生命周期. 同时由于不持有观察者的全部权, 以是将访问限定为私有, 使得外界均无法访问,
  • 函数 subscribe() 用以添加新的观察者, 参数为 shared_ptr, 添加时自动转换为 weak_ptr.
然后是 notify() 函数和 unsubscribe() 函数的实现:
  1. template <typename T>
  2. struct Observable {
  3. private:
  4.         std::vector<std::weak_ptr<Observer<T>>> observers_; // 存储弱引用
  5. public:
  6.         void subscribe(std::shared_ptr<Observer<T>> f) {
  7.                 observers_.emplace_back(f); // 自动转换为 weak_ptr
  8.         }
  9.         void unsubscribe(const std::shared_ptr<Observer<T>>& f) {
  10.                 observers_.erase(
  11.                         std::remove_if(observers_.begin(), observers_.end(),
  12.                                 [&](const auto& weak_obs) {
  13.                                         auto obs = weak_obs.lock();  // 尝试升级为 shared_ptr
  14.                                         return !obs || obs == f;     // 清理失效或匹配的观察者
  15.                                 }),
  16.                         observers_.end()
  17.                 );
  18.         }
  19.         void notify(T& source, const string& name) {
  20.                 auto it = observers_.begin();
  21.                 while (it != observers_.end()) {
  22.                         if (auto observer = it->lock()) {
  23.                                 observer->field_changed(source, name);
  24.                                 ++it;
  25.                         } else {
  26.                                 it = observers_.erase(it); // 自动清理失效观察者
  27.                         }
  28.                 }
  29.         }
  30. };
复制代码
notify() 函数用于订阅, it为当前迭代器, 循环体内 lock()实行获取有效 shared_ptr, 乐成时实行关照并进步迭代器, 失败时擦除当前元素并获取新迭代器.
unsubscribe() 函数接纳双重整理, weak_obs.lock() 查抄观察者是否已失效, obs == f 匹配具体要移除的观察者, 利用 erase-remove 惯用法包管容器利用安全.
如今我们完成了抽象观察者类, 继续下去, 做出具体实现, 新的Person 类和 get_age() 函数将实现如下:
  1. struct Person : Observable<Person> {
  2. private:
  3.         int age_;
  4. public:
  5.         explicit Person(const int age) : age_{age} {}
  6.         ~Person() = default;
  7.         [[nodiscard]] int get_age() const {
  8.                 return age_;
  9.         }
  10.         void set_age(const int age) {
  11.                 if (this->age_ != age) {
  12.                         age_ = age;
  13.                         notify(*this, "age");
  14.                 }
  15.         }
  16. };
复制代码
在 set_age() 中调用 notify() 为全部的观察者做出关照.

毗连观察者和被观察者

如今我们要利用我们之前构建的观察者和被观察者, 我们的具体观察者如下:
  1. struct ConsolePersonObserver final : public Observer<Person> {
  2.         void field_changed(Person& source, const string& field_name) override {
  3.                 cout << "Person's " << field_name << "has changed to " << source.get_age()
  4.                          << "." << endl;
  5.         }
  6. };
复制代码
他和之前我们的实现是一样的, 在这里只是再次给出,
然后就是测试用例:
  1. void test() {
  2.         Person p{20};
  3.         const auto cpo = make_shared<ConsolePersonObserver>();
  4.         p.subscribe(cpo);
  5.         p.set_age(21);
  6.         p.set_age(22);
  7. }
复制代码
将输出如下:
   Person’s agehas changed to 21.
Person’s agehas changed to 22.
  依赖标题

我们继续引入一些标题, 假设有一个推选需求.
大于16岁的人具有推选权,因此当某个人具有推选权之后我们盼望被关照到。起首,我们假设Person类有如下的getter函数:
  1. bool get_can_vote() const {
  2.         return age_ >= 16;
  3. }
复制代码
显而易见这个函数依赖于 age_ 而触发, 以是我们如果按照之前的想法, 为他设定一个 setter 这并没有须要, 我们可以将这个利用同步到之前关于 age 的 setter 也就是 age_setter中一起办理掉.
然后我们再给出 Person 类的新实现:
  1. struct Person : Observable<Person> {
  2. private:
  3.         int age_;
  4. public:
  5.         explicit Person(const int age) : age_{age} {}
  6.         ~Person() = default;
  7.         [[nodiscard]] int get_age() const {
  8.                 return age_;
  9.         }
  10.         bool get_can_vote() const { return age_ >= 16; }
  11.         void set_age(const int value) {
  12.                 if (age_ != value) {
  13.                         const auto old_can_vote = get_can_vote(); // store old value
  14.                         age_ = value;
  15.                         notify(*this, "age");
  16.                         if (old_can_vote != get_can_vote()) // check value has changed
  17.                                 notify(*this, "can_vote");
  18.                 }
  19.         }
  20. };
复制代码
如许看来 set_age 好像长得过于痴肥了。我们不但查抄年事是否改变,同时也查抄can_vote是否改变,并给出关照.
大概会以为这种方法不能很好地扩展, 想象一下 can_vote 依赖于两个字段,好比 age 和 citizenship —— 这意味着这两个属性的 setter 都必须处置惩罚 can_vote 关照。
更糟糕的是,如果年事也会以这种方式影响其他10种属性呢? 这是一个不可用的办理方案,它会导致脆弱的代码无法维护,由于变量之间的关系须要手动跟踪。
坦白地说,在前一种情况下,can_vote 属性依赖 age 属性。依赖性属性的挑衅本质上是 Excel 等工具的挑衅:给定差别单位格之间的大量依赖性,当此中一个单位格发生厘革时,您怎样知道哪些单位格须要重新盘算。
固然,属性依赖关系可以被情势化为某种范例的map<string, vector<string>>。这将保存一个受属性影响的属性列表(大概相反,影响属性的全下属性)。但是,这个map必须手工界说,而且要与实际代码保持同步非常贫困。
取消订阅和线程安全

如今转头看取消订阅的实现:
  1. void unsubscribe(const std::shared_ptr<Observer<T>>& f) {
  2.         observers_.erase(
  3.                 std::remove_if(observers_.begin(), observers_.end(),
  4.                                [&](const auto& weak_obs) {
  5.                                        auto obs = weak_obs.lock(); // 尝试升级为 shared_ptr
  6.                                        return !obs || obs == f;    // 清理失效或匹配的观察者
  7.                                }),
  8.                 observers_.end()
  9.         );
  10. }
复制代码
我们接纳了算法 erase-remove_if, 对于 vector 这并不线程安全, 同时调用 subscribe() 和 unsubscribe() 大概会导致意想不到的结果,由于这两个函数都会修改 vector.
固然一个易行的办理方案是加锁:
  1. template <typename T>
  2. struct Observable {
  3.         void notify(T& source, const string& name) {
  4.                 scoped_lock<mutex> lock{mtx};
  5.                 ...
  6.         }
  7.         void subscribe(Observer<T>* f) {
  8.                 scoped_lock<mutex> lock{mtx};
  9.                 ...
  10.         }
  11.         void unsubscribe(Observer<T>* o) {
  12.                 scoped_lock<mutex> lock{mtx};
  13.                 ...
  14.         }
  15. private:
  16.         vector<Observer<T>*> observers;
  17.         mutex mtx;
  18. };
复制代码
另一个非常可行的替换方案是利用类似TPL/PPL的concurrent_ vector。固然, 会失去排序包管(换句话说,一个接一个地添加两个对象并不能包管它们按照谁人序次得到关照),但不必自己管理锁。
可重入

末了一种实现提供了一些线程安全性,只要有人须要,就锁定这三个关键方法中的任何一个。但是如今让我们假想以了局景:您有一个交通管理组件,它不绝监督一个人,直到他到了可以开车的年事。当他们17岁时,组件就会取消订阅:
  1. struct TrafficAdministration final : Observer<Person> {
  2.         void TrafficAdministration::field_changed(Person& source,
  3.                                                   const string& field_name) override {
  4.                 if (field_name == "age") {
  5.                         if (source.get_age() < 17)
  6.                                 cout << "Whoa there, you are not old enough to drive!\n";
  7.                         else {
  8.                                 // oh, ok, they are old enough, let's not monitor them anymore
  9.                                 cout << "We no longer care!\n";
  10.                                 const auto temp = make_shared<TrafficAdministration>();
  11.                                 source.unsubscribe(temp);
  12.                         }
  13.                 }
  14.         }
  15. };
复制代码
这将会出现一个标题,由于当或人17岁时,整个调用链将会是:
   notify() —> field_changed() —> unsubscribe()
  这是一个标题,由于在unsubscribe()中,我们终极试图获取一个已经被获取的锁。这是一个可重入标题。处置惩罚这件事有差别的方法:


  • 一种方法是简单地克制这种情况。毕竟,至少在这个特定的例子中,很显着这里发生了可重入性
  • 另一种方法是放弃从聚集中删除元素的想法。相反,我们可以如许写:
  1. void unsubscribe(Observer<T>* o) {
  2.         auto it = find(observers_.begin(), observers_.end(), o);
  3.         if (it != observers_.end())
  4.                 *it = nullptr; // cannot do this for a set
  5. }
复制代码
随后,当利用notify()时,只须要举行额外的查抄:
  1. void notify(T& source, const string& name) {
  2.         for (auto&& obs : observers_)
  3.                 if (obs)
  4.                         obs->field_changed(source, name);
  5. }
复制代码
通过 Boost.Signals2 来实现 Observer

然后作者又是很经典地用了 boost 库, 这里直接贴原文
观察者模式有许多预打包的实现,而且大概最闻名的是 Boost.Signals2 库。本质上,该库提供了一种称为信号的范例,它表现 C++ 中的信号术语(在别处称为事故)。可以通过提供函数或 lambda 表达式 来订阅此信号。它也可以被取消订阅,当你想关照它时,它可以被排除。
  1. template <typename T>
  2. struct Observable {
  3.   signal<void(T&, const string&)> property_changed;
  4. };
复制代码
它的调用如下所示:
  1. struct Person : Observable<Person> {
  2.   void set_age(const int age) {
  3.     if (this->age == age) return;
  4.     this->age = age;
  5.     property_changed(*this, "age");
  6.   }
  7. };
复制代码
API 的实际利用将直接利用信号,固然,除非你决定添加更多 API 陷阱以使其更容易:
  1. Person p{123};
  2. auto conn = p.property_changed.connect([](Person&, const string& prop_name) {
  3.   cout << prop_name << " has been changed" << endl;
  4. });
  5. p.set_age(20);  // name has been changed
  6. // later, optionally
  7. conn.disconnect();
复制代码
connect() 调用的结果是一个毗连对象,它也可以用于在你不再须要信号关照时取消订阅。
总结

何时须要利用观察者模式?

观察者模式的焦点应用场景是 实现对象间的一对多依赖关系,当被观察对象(Subject)的状态厘革须要自动关照多个依赖对象(Observer)时利用。具体需求场景包罗:

  • 事故驱动体系:

    • GUI框架中按钮点击事故、数据模子厘革触发视图更新。
    • 示例:用户点击按钮后,关照多个UI组件革新状态。

  • 及时数据分发:

    • 传感器数据更新时,关照多个监控监控模块(如温度传感器→表现屏、报警器)。

  • 发布-订阅机制:

    • 消息队列中生产者发布消息,多个斲丧者订阅并处置惩罚消息。
    • 示例:消息推送体系,用户订阅主题后吸取干系消息。


观察者模式办理的焦点标题


  • 紧耦合标题:

    • 制止被观察者直接调用具体观察者的方法,淘汰对象间的直接依赖。

  • 动态关照机制:

    • 支持运行时动态添加或移除观察者,加强体系机动性。

  • 状态同步:

    • 确保多个观察者与被观察者的状态保持划一,制止手动同步的复杂性。


与其他计划模式的协同利用

观察者模式常与其他模式联合,以优化体系计划:
模式协同场景示例中介者模式通过中介者同一管理观察者和被观察者的交互,低沉对象间直接通讯的复杂度。谈天室中,用户(观察者)的消息由中介者广播给其他用户,而非直接相互引用。状态模式被观察者的状态厘革触发观察者的活动调解,两者联合实近况态驱动的相应逻辑。订单状态变为“已发货”时,关照物流体系和用户界面更新表现。下令模式将观察者的相应利用封装为下令对象,支持取消、重做等扩展功能。用户点击按钮(触发下令对象),下令实行后关照日记模块纪录利用。装饰器模式动态加强观察者的功能(如添加日记、缓存),而无需修改被观察者代码。日记观察者被装饰器包装,在纪录日记前添加时间戳或IP信息。
与其他模式的对比



  • 发布-订阅模式:

    • 观察者模式:被观察者直接维护观察者列表,无中心事故通道(强耦合)。
    • 发布-订阅模式:通过事故总线解耦发布者和订阅者(松耦合)。

  • 中介者模式:

    • 中介者模式:会合管理对象间交互,制止多对多通讯。
    • 观察者模式:实现一对多的单向关照机制。


经典应用场景


  • MVC架构:

    • 模子(Model)作为被观察者,视图(View)作为观察者,模子数据厘革时自动更新视图。

  • 及时监控监控体系:

    • 服务器资源(CPU、内存)状态厘革时,关照监控监控仪表盘和报警体系。

  • 股票生意业务体系:

    • 股票代价变更时,关照生意业务算法、用户界面和风险控制模块。

  • 游戏引擎:

    • 脚色血量厘革时,触发UI血条更新、音效播放和存档自动生存。


实现步调与关键点


  • 界说观察者接口:

    • 声明更新方法(如update()),供具体观察者实现。

  • 界说被观察者接口:

    • 提供注册(attach())、注销(detach())和关照(notify())方法。

  • 具体实现:

    • 被观察者维护观察者列表,状态厘革时调用notify()遍历列表触发观察者更新。

注意事项


  • 内存管理:

    • 被观察者持有观察者的指针时,需确保观察者生命周期公道,制止悬垂指针。

  • 性能优化:

    • 当观察者数目巨大时,关照利用大概成为性能瓶颈,可通过异步关照或批量处置惩罚优化。

  • 循环依赖:

    • 制止观察者和被观察者相互引用,导致内存走漏或死锁。


观察者模式是 事故驱动的纽带,其焦点代价在于:


  • 解耦与扩展性:分离事故源与事故处置惩罚器,支持动态添加观察者。
  • 及时相应:确保状态厘革即时关照全部依赖对象。
  • 通用性:实用于GUI、监控、消息体系等广泛场景。
通过与其他模式(如中介者、状态)的协同,观察者模式可以大概构建高相应、低耦合的体系架构,是当代软件计划中实现事故处置惩罚的焦点工具。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表