【设计模式精讲】24.观察者模式(Observer)

【设计模式精讲】24.观察者模式(Observer)

【摘要】:传感器每来一个新样本,当前值面板、统计面板、告警模块都该刷新------直觉的写法是在 onSample 里逐个调用三个面板,订阅者从此焊死在发布者体内,加一块面板改一次传感器。本文从 RG 的经典气象站讲起,给出观察者的 GoF 意图:一对多依赖、状态一变、全体自动通知------发布者对订阅者的认知降到「一个接口」为止;C++98 版覆盖 notify 的重入副本、推与拉两种模型,现代版给出 std::function 槽位、RAII 订阅令牌与 weak_ptr 三件套,把「观察者悬垂」这个 C++ 最著名的坑逐一封死。文末对照 Boost.Signals2、C++20 的 std::stop_callback 与 AOSP 传感器事件队列。这是 23 种模式里知名度最高的一个,也是生命周期陷阱最多的一个。

【关键词】:观察者、发布订阅、事件通知、回调、悬垂指针、信号槽

【代码基准】:C++17

1. 每加一块面板,就改一次传感器

工控面板上接着一个温度传感器,新样本到来时要刷新三处:当前值显示、历史统计、超温告警。第一版代码顺手就这样写:

cpp 复制代码
// 说明性片段
// ❌ 订阅者名单焊死在发布者体内
void Sensor::onSample(double t) {
  temp_ = t;
  currentPanel_->update(t);   // 认识面板一
  statsPanel_->update(t);     // 认识面板二
  alertModule_->update(t);    // 认识面板三
}

这是 Refactoring Guru 讲观察者时用的气象站场景的工控版,病灶也如出一辙:传感器被迫认识每一块面板------每加一个订阅者(比如「数据落盘」),就要改一次传感器;面板想换个数据源(接压力传感器),也要改面板自己;「谁关心温度变化」这个本该运行时增减的名单,被编译进了发布者的类型里。反方向的方案是轮询:各面板定时来拉------白白浪费了不变化时的全部开销,又牺牲了实时性。

结构性诊断:状态的所有者与状态的消费者被直接焊死。传感器(拥有状态)与面板(消费状态)之间缺一份运行时可增减的订阅关系。观察者模式补的正是这份关系------顺带把发布者对订阅者的认知压缩到极限:一个接口。

2. 模式意图与定义

  • 一句话定义 :定义对象间的一种一对多依赖,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
    解决的问题 :状态变化需要通知一组动态增减、互不相识的关心者,而状态的所有者不想认识他们中的任何一个。
  • GoF 原文意图Define a one-to-many dependency between objects so that when an object changes state, all its dependents are notified and updated automatically. (定义对象间一对多的依赖关系。)关键词是 one-to-manyautomatically------前者区别于责任链的一对一传递(第 18 篇),后者划清与轮询的界限。
  • Refactoring Guru 的表述:观察者模式允许你定义一种订阅机制,可在对象事件发生时通知多个「观察」该对象的其他对象------RG 用气象站做例子:气象站(发布者)拥有数据,各种展示面板(订阅者)各取所需,增删面板互不打扰。

三条定性,条条对应后面的工程要点:

  1. 发布者的认知降到「一个接口」 ------Sensor 只认 Panel*,不知道面板是谁、有几个、会做什么。这是第 2 篇依赖倒置的运行时形态:抽象(接口)拥有实现(订阅者名单),实现反过来依赖抽象。
  2. 推与拉 :通知可以update(temp) 把数据带过去,快、省一次往返,但接口被数据形状绑死),也可以update(subject) 只递名片,观察者自己按需取------subject->temp())。粒度小且稳定用推,字段多且各取所需用拉。
  3. C++ 的特有难题是生命周期------通知本质是一次回调,回调的对象可能已经析构。悬垂、重入、注销遗漏,三坑贯穿第 4、5 两节,也是 C++ 观察者与垃圾回收语言观察者的分水岭。

3. UML 图 + 结构说明

#mermaid-svg-SXn0C3JS0zbb2awx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SXn0C3JS0zbb2awx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SXn0C3JS0zbb2awx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SXn0C3JS0zbb2awx .error-icon{fill:#552222;}#mermaid-svg-SXn0C3JS0zbb2awx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SXn0C3JS0zbb2awx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SXn0C3JS0zbb2awx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SXn0C3JS0zbb2awx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SXn0C3JS0zbb2awx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SXn0C3JS0zbb2awx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SXn0C3JS0zbb2awx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SXn0C3JS0zbb2awx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SXn0C3JS0zbb2awx .marker.cross{stroke:#333333;}#mermaid-svg-SXn0C3JS0zbb2awx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SXn0C3JS0zbb2awx p{margin:0;}#mermaid-svg-SXn0C3JS0zbb2awx g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-SXn0C3JS0zbb2awx g.classGroup text .title{font-weight:bolder;}#mermaid-svg-SXn0C3JS0zbb2awx .cluster-label text{fill:#333;}#mermaid-svg-SXn0C3JS0zbb2awx .cluster-label span{color:#333;}#mermaid-svg-SXn0C3JS0zbb2awx .cluster-label span p{background-color:transparent;}#mermaid-svg-SXn0C3JS0zbb2awx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SXn0C3JS0zbb2awx .cluster text{fill:#333;}#mermaid-svg-SXn0C3JS0zbb2awx .cluster span{color:#333;}#mermaid-svg-SXn0C3JS0zbb2awx .nodeLabel,#mermaid-svg-SXn0C3JS0zbb2awx .edgeLabel{color:#131300;}#mermaid-svg-SXn0C3JS0zbb2awx .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-SXn0C3JS0zbb2awx .label text{fill:#131300;}#mermaid-svg-SXn0C3JS0zbb2awx .labelBkg{background:#ECECFF;}#mermaid-svg-SXn0C3JS0zbb2awx .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-SXn0C3JS0zbb2awx .classTitle{font-weight:bolder;}#mermaid-svg-SXn0C3JS0zbb2awx .node rect,#mermaid-svg-SXn0C3JS0zbb2awx .node circle,#mermaid-svg-SXn0C3JS0zbb2awx .node ellipse,#mermaid-svg-SXn0C3JS0zbb2awx .node polygon,#mermaid-svg-SXn0C3JS0zbb2awx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SXn0C3JS0zbb2awx .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-SXn0C3JS0zbb2awx g.clickable{cursor:pointer;}#mermaid-svg-SXn0C3JS0zbb2awx g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-SXn0C3JS0zbb2awx g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-SXn0C3JS0zbb2awx .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-SXn0C3JS0zbb2awx .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-SXn0C3JS0zbb2awx .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-SXn0C3JS0zbb2awx .dashed-line{stroke-dasharray:3;}#mermaid-svg-SXn0C3JS0zbb2awx .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-SXn0C3JS0zbb2awx #compositionStart,#mermaid-svg-SXn0C3JS0zbb2awx .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-SXn0C3JS0zbb2awx #compositionEnd,#mermaid-svg-SXn0C3JS0zbb2awx .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-SXn0C3JS0zbb2awx #dependencyStart,#mermaid-svg-SXn0C3JS0zbb2awx .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-SXn0C3JS0zbb2awx #dependencyStart,#mermaid-svg-SXn0C3JS0zbb2awx .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-SXn0C3JS0zbb2awx #extensionStart,#mermaid-svg-SXn0C3JS0zbb2awx .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-SXn0C3JS0zbb2awx #extensionEnd,#mermaid-svg-SXn0C3JS0zbb2awx .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-SXn0C3JS0zbb2awx #aggregationStart,#mermaid-svg-SXn0C3JS0zbb2awx .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-SXn0C3JS0zbb2awx #aggregationEnd,#mermaid-svg-SXn0C3JS0zbb2awx .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-SXn0C3JS0zbb2awx #lollipopStart,#mermaid-svg-SXn0C3JS0zbb2awx .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-SXn0C3JS0zbb2awx #lollipopEnd,#mermaid-svg-SXn0C3JS0zbb2awx .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-SXn0C3JS0zbb2awx .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-SXn0C3JS0zbb2awx .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SXn0C3JS0zbb2awx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SXn0C3JS0zbb2awx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SXn0C3JS0zbb2awx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 只认接口的订阅名单
Sensor
-panels_ vector
-temp_ double
+attach(p) : void
+detach(p) : void
+setTemp(t) : void
-notify() : void
<<interface>>
Panel
+update(temp) : void
CurrentPanel
+update(temp) : void
StatsPanel
+update(temp) : void
AlertModule
+update(temp) : void

四个参与者(GoF 命名):

  • 主题(Subject)Sensor------持有一份「只认接口」的观察者名单,提供 attach/detach 增减订阅,状态变化时 notify
  • 观察者(Observer)接口Panel,声明更新通道 update------推模型带数据,拉模型带主题指针;
  • 具体观察者:三块面板,各自对同一事件做出互不相同、互相不知道的反应;
  • 具体主题 :状态真正变化的地方,何时 通知由它定,通知之后发生什么它一无所知。

结构定性:观察者是无意志的广播台 ------喊一嗓子「温度变了」,听众是谁、听完做什么,台里一概不管。这个定性是与中介者(第 22 篇)辨析的锚点:中介者听到报告要决策并指挥 多个对象,观察者只负责送达 。也因此观察者的耦合是所有「中间人模式」里最松的:主题与观察者之间只有一条 update 通道,谁也不指挥谁。

4. 传统 C++ 写法(C++11 之前)

GoF 原书时代的形态:观察者名单存裸指针、notify 遍历副本抗重入、推模型递数据:

cpp 复制代码
// C++98/03 写法
#include <cstdio>
#include <vector>

// ---- 观察者接口 ----
class Panel {
public:
  virtual ~Panel() {}
  virtual void update(double temp) = 0;

protected:
  Panel() {}

private:
  Panel(const Panel&);
  Panel& operator=(const Panel&);
};

// ---- 主题 ----
class Sensor {
public:
  Sensor() : temp_(0) {}

  void attach(Panel* p) {
    panels_.push_back(p);      // 只借不拥
  }

  void detach(Panel* p) {
    for (size_t i = 0; i < panels_.size();
         ++i) {
      if (panels_[i] == p) {
        panels_.erase(
            panels_.begin() + i);
        return;
      }
    }
  }

  void setTemp(double t) {
    temp_ = t;
    notify();                  // 状态变化即广播
  }

private:
  void notify() {
    // 遍历副本:允许观察者在回调里
    // 增删订阅而不失效迭代
    std::vector<Panel*> snap = panels_;
    for (size_t i = 0; i < snap.size();
         ++i) {
      snap[i]->update(temp_);  // 推模型
    }
  }

  std::vector<Panel*> panels_;
  double temp_;
};

// ---- 三个观察者:对同一事件各做各事 ----
class CurrentPanel : public Panel {
public:
  void update(double t) {
    printf("当前温度:%.1f\n", t);
  }
};

class StatsPanel : public Panel {
public:
  StatsPanel() : sum_(0), n_(0) {}

  void update(double t) {
    sum_ += t;
    ++n_;
    printf("平均温度:%.1f\n",
           sum_ / n_);
  }

private:
  double sum_;
  int n_;
};

class AlertModule : public Panel {
public:
  void update(double t) {
    if (t > 100.0)
      printf("告警:温度过高!\n");
  }
};

int main() {
  Sensor sensor;
  CurrentPanel cur;
  StatsPanel stats;
  AlertModule alert;

  sensor.attach(&cur);
  sensor.attach(&stats);
  sensor.attach(&alert);

  sensor.setTemp(25.0);    // 三处各刷各的
  sensor.setTemp(120.0);   // 含一条告警
  return 0;
}

对照第 1 节:加「数据落盘」订阅者 = 写一个 Panel 派生类加一行 attach,传感器零改动;换数据源 = 面板 attach 到新传感器,面板零改动。

三条传统写法的铁律,全是拿事故换来的:notify 遍历副本 ------某个观察者在回调里 detach 自己甚至 attach 新面板,正在遍历的容器立即被「边走边抽地板」弄崩溃,副本是 C++98 时代最朴素的护栏(更严格的方案是延迟应用增删,见第 5 节);名单只借不拥 ------panels_ 存裸指针,传感器不负责面板生死;析构必须退订 ------面板销毁前忘了 detach,传感器的下一次 notify 就是对亡魂的调用,这是 C++ 观察者最著名的悬垂坑,第 5 节的两件现代工具专为封它而来。

5. 现代 C++ 进阶写法

升级零:std::function 槽位,订阅匿名化。观察者连接口都不用实现了------一个可调用对象就是一个订阅:

cpp 复制代码
// 节选:槽位化的发布总线
#include <algorithm>
#include <functional>
#include <utility>
#include <vector>

class TempBus {
public:
  using Slot = std::function<void(double)>;
  using Id = int;

  Id subscribe(Slot s) {
    slots_.emplace_back(next_,
                        std::move(s));
    return next_++;
  }

  void unsubscribe(Id id) {
    slots_.erase(
        std::remove_if(
            slots_.begin(), slots_.end(),
            [id](const auto& p) {
              return p.first == id;
            }),
        slots_.end());
  }

  void publish(double v) {
    auto snap = slots_;      // 副本抗重入
    for (auto& [id, s] : snap)
      if (s)
        s(v);                // 逐槽回调
  }

private:
  std::vector<std::pair<Id, Slot>> slots_;
  int next_ = 0;
};

订阅者从「实现接口的类」降格为「一段可调用代码」------subscribe 返回的 Id 是唯一的退订凭证,弄丢它就再也退不了(下一件工具解决)。

改进一:RAII 订阅令牌------注销不再靠自觉。把「退订凭证」做成析构即退订的移动专用对象,第 4 节「析构必须退订」的铁律从注释升格为类型保证:

cpp 复制代码
// 节选:与 TempBus 配套的订阅令牌
class Subscription {
public:
  Subscription(TempBus& bus, TempBus::Id id)
      : bus_(&bus), id_(id) {}

  ~Subscription() {
    if (bus_)
      bus_->unsubscribe(id_);   // 忘退订?
                                // 不存在
  }

  Subscription(Subscription&& o) noexcept
      : bus_(o.bus_), id_(o.id_) {
    o.bus_ = nullptr;           // 移交后失能
  }

  Subscription& operator=(
      Subscription&&) = delete;
  Subscription(const Subscription&) = delete;

private:
  TempBus* bus_;
  TempBus::Id id_;
};

// 用法:面板把令牌存成成员
// class StatsPanel {
//   Subscription sub_;
//  ...构造时 sub_{bus, bus.subscribe(...)}
// 面板析构 → 令牌析构 → 自动退订

这是 GoF 没能写进模式图里的 C++ 补丁:订阅关系本身也该有生命周期

改进二:weak_ptr 观察------通知前确认活着 。观察者由 shared_ptr 管理时,总线改存 weak_ptr,第 16 篇 onTimer 的门卫模式整体迁入:

cpp 复制代码
// 节选:weak 观察名单
#include <memory>
#include <vector>

class Sensor2 {
public:
  void attach(std::shared_ptr<Panel> p) {
    panels_.push_back(std::move(p));
  }

  void publish(double v) {
    for (auto& wp : panels_) {
      if (auto p = wp.lock())   // 活着才通知
        p->update(v);
    }
    // 顺带清理过期的空壳
    panels_.erase(
        std::remove_if(
            panels_.begin(),
            panels_.end(),
            [](const auto& wp) {
              return wp.expired();
            }),
        panels_.end());
  }

private:
  std::vector<std::weak_ptr<Panel>> panels_;
};

三件套(槽位、令牌、weak)不必全上:小项目槽位 + 手工退订即可;跨模块长生命周期总线,令牌或 weak 至少其一------悬垂回调的排查成本,永远高于多写一个包装类的成本。

展望 :跨进程的观察者就是发布订阅消息队列 ------经 broker 中转、按主题过滤、收发双方互不知晓,意图与本篇同源、基础设施完全不同,别把进程内的 vector<Panel*> 硬改成进程间的;C++ 没有反射,图形界面框架的「信号槽」要靠元对象代码生成(Qt 的 moc)或宏------那是观察者加上一层字符串级联表,本质未变。

6. 优缺点与适用场景

  • ✅ 优点(GoF 后果清单):运行时建立/解除的抽象耦合 ------主题只认一个接口,观察者随时增减,双方互不拖累;广播通信 ------一次状态变化自动达达所有关心者,无需轮询;支持事件级联------观察者的响应可以是修改另一个主题,事件沿依赖链传播,形成反应式结构。
  • ❌ 缺点:通知风暴与级联失控 ------正是上一条优点的暗面:A 通知 B、B 又通知 C,几跳之后没人知道这次更新从哪来、会到哪去,观察者的「面条」比调用的面条更难调试(断点跳进一串匿名槽位);通知顺序无保证 ------依赖特定顺序的观察者逻辑,等于在图上埋了看不见的边;生命周期三坑(悬垂、重入、注销遗漏)在 C++ 里全靠自己------三件套之外别无银弹;每次通知一层虚调用/间接调用,热路径高频事件要掂量开销。
  • 🎯 适用场景:一对多且订阅者动态增减的进程内事件------GUI 事件、属性绑定、缓存失效、配置热更新、遥测埋点;主题不关心(也不该关心)订阅者的行为;变化频率低于「每变化都全量轮询」的成本线。

〔辨析〕观察者 vs 中介者(第 22 篇,正面回收):同是星型,无意志的广播台 vs 有意志的协调者 ------观察者送达即结束,中介者听完还要决策指挥;「事件后要不要指挥多个对象」是判别问题;两者常合体(中介者订阅同事的事件)。观察者 vs 责任链(第 18 篇):一对多广播不终止 vs 一对一传递认领即停;广播说「我变了」,传递问「归你管吗」。观察者 vs 发布订阅:同源异构------观察者是进程内、点对点名单、收发双方经接口相识;发布订阅是跨进程/跨模块、经 broker、按主题过滤、收发互不知晓;工程上常把前者当后者的进程内退化形态。观察者 vs 迭代器(第 21 篇):迭代器「拉」式逐个取,观察者「推」式批量送------数据流向相反的两类协议。

7. 开源项目中的身影

Boost.Signals2:C++ 信号槽的工业完全体signal 是主题、connect 是订阅,而它真正值钱的是连接管理:scoped_connection 就是第 5 节订阅令牌的成品,track 系列把 weak 观察做成了声明式配置:

cpp 复制代码
// 说明性片段(需包含 boost/signals2)
#include <boost/signals2/signal.hpp>
namespace sig2 = boost::signals2;

sig2::signal<void(double)> tempChanged;

// RAII 连接:作用域结束自动断开
sig2::scoped_connection c =
    tempChanged.connect(
        [](double t) { /* 刷新面板 */ });

tempChanged(25.0);   // signal 本身可调用

点评:Signals2 把本文第 5 节三件套全部内建,还处理了重入(block/递归连接控制)与线程安全(mutex 互斥集)。代价是每个 signal 的体积与连接开销------游戏引擎里它常被手写轻量总线替换,取舍点还是那三坑的成本。

标准库:std::stop_callback,一次性观察者的现身。C++ 标准库一直没有通用信号槽,却在 jthread 的停止协议里内置了一个微型观察者------「停止请求」是事件,回调在事件发生时被触发恰好一次:

cpp 复制代码
// C++20 片段
#include <thread>

std::stop_source src;
std::stop_callback cb(
    src.get_token(),
    [] { /* 收到停止请求,收摊 */ });

// 某处调用 src.request_stop()
// → 回调触发一次,cb 析构自动注销

点评:麻雀虽小,主角齐全:订阅令牌(cb 析构即注销)、单次语义、线程安全送达。标准库的选择很说明问题------通用信号槽留给库生态,协议级的通知才进标准

AOSP:传感器事件队列,气象站的工业版 。Android 的传感器框架就是本篇主例的放大:应用注册监听器,传感器数据经 InputDispatcher 同族的事件机制回调 onSensorChanged;native 侧 NDK 的 ASensorEventQueue 把「订阅 + 事件回调 + Looper 唤醒」打包成一个队列:

cpp 复制代码
// 说明性片段(节选自 NDK sensor API,简化)
ASensorEventQueue* q =
    ASensorManager_createEventQueue(
        mgr, looper, LOOPER_ID_USER,
        nullptr /*回调*/, nullptr);

ASensorEventQueue_enableSensor(
    q, accelerometer);   // attach

// Looper 被数据唤醒后逐条分发事件,
// 等价于 notify() 的循环

点评:注意它与第 19 篇的会师------事件队列里流动的既可以说是命令(MessageHandler),也可以说是通知(唤醒回调);发布订阅与命令物化在系统底层本就是同一套基础设施的两面。

三份代码合看:Signals2 把三坑全封、stop_callback 小而严谨、AOSP 把观察者做进系统唤醒机制------「一对多、送达到、别管他」这九个字,撑起了从桌面 GUI 到传感网络的整片事件世界。

本篇小结

状态的所有者不必认识状态的消费者:一份只认接口的订阅名单、一条 update 通道,通知自动送达所有关心者,名单运行时随意增删------这是依赖倒置最日常的运行时形态。C++ 把这个「知名度最高」的模式同时变成了「陷阱最多」的模式:重入要副本护栏、注销要 RAII 令牌、悬垂要 weak 门卫,三件套按规模选配;跨进程时换成消息队列,别硬撑进程内结构。与中介者的分水岭再念一遍:无意志的广播台只管送达,有意志的协调者还要指挥。下一篇状态模式,镜头转回被通知的那一侧:当对象自己的行为随状态大变、满屏 switch 开始疯长时,把每个状态变成一个对象。

本文模式定义与角色划分参考了 Refactoring Guru《设计模式》中文版「观察者」一章,意图译文、推拉模型与后果清单参考了 GoF《Design Patterns》第 5 章 Observer 一节。

相关推荐
imgsq1 小时前
S-57 数据解剖:把一个 ENC 文件拆给你看
c++·数据可视化
迷茫、Peanut2 小时前
中断的时钟
c++
2402_882893862 小时前
用哈希表封装 unordered_map 和 unordered_set —— 手撕 C++ 哈希容器
c++·哈希桶·unordered_map·unordered_set
熊猫钓鱼>_>2 小时前
用 OHAudioSuite 空间渲染节点搭一座「3D 有声博物馆」——从三种模式到 FreeBuds 头追的实战记录
c++·3d·ai·harmonyos·arkts·audio·ohaudiosuite
_约书亚_2 小时前
chapter 9 高级线程管理 — 归纳总结
c++
linx2952 小时前
阶段三讲义:类型与对象(Classes & Essential Operations)
c++
飞鸟真人2 小时前
C++ lambda 捕获完整梳理
c++·闭包·lamda
東隅已逝,桑榆非晚3 小时前
c++内存管理
c++·笔记
是个西兰花3 小时前
C++11:线程库与线程安全问题
开发语言·c++