# C++设计模式:观察者模式——构建优雅的事件驱动系统 在软件开发中,对象之间经常存在这样一种关系:**一个对象的状态发生变化时,其他若干对象需要自动做出响应**。 ### 一个真实的场景 假设你在开发一个股票交易系统,当股价发生变化时,多个模块需要同时响应: ``` class StockPrice { public: void setPrice(double newPrice) { price_ = newPrice; // 通知所有相关方... 怎么通知? display_.update(price_); // 更新显示界面 alertSystem_.check(price_); // 检查是否触发警报 logger_.log(price_); // 记录价格日志 analyzer_.analyze(price_); // 实时分析 trader_.evaluate(price_); // 自动交易评估 } private: double price_; DisplayPanel display_; // 显示模块 AlertSystem alertSystem_; // 警报模块 PriceLogger logger_; // 日志模块 PriceAnalyzer analyzer_; // 分析模块 AutoTrader trader_; // 自动交易模块 }; ``` **问题立刻暴露**: **问题 1:高度耦合** ``` StockPrice 必须知道所有观察者的存在: ┌─────────────┐ │ StockPrice │──→ DisplayPanel │ │──→ AlertSystem │ 知道所有人! │──→ PriceLogger │ │──→ PriceAnalyzer │ │──→ AutoTrader └─────────────┘ 新增一个模块 → 修改 StockPrice 的代码 删除一个模块 → 修改 StockPrice 的代码 修改接口 → 修改 StockPrice 的代码 ``` **问题 2:违反开闭原则** ``` // 新增一个"推送到手机App"的功能 void setPrice(double newPrice) { price_ = newPrice; display_.update(price_); alertSystem_.check(price_); logger_.log(price_); analyzer_.analyze(price_); trader_.evaluate(price_); mobileApp_.push(price_); // ← 必须修改源码! } ``` 每增加一个新的响应模块,`StockPrice` 的代码就要被修改一次。核心数据类变成了一个"什么都知道"的胖类。 **问题 3:无法动态管理** ``` // 运行时想暂时关闭自动交易? // 运行时想新增一个监控模块? // 做不到!所有关系都在编译期硬编码了 ``` 这些问题的本质是:**数据的产生者(Subject)和数据的消费者(Observer)之间的耦合太紧**。 在生活中,这种关系无处不在: - • **微信公众号**就是典型的观察者模式——你关注了一个公众号(订阅),公众号发文时你会收到推送(通知)。你可以随时取消关注(取消订阅),公众号不需要知道你是谁。 - • **闹钟**是时间的观察者——你设定好时间(订阅某个事件),到点了闹钟响铃(收到通知)。你可以设置多个闹钟(多个观察者),每个独立响应。 - • **消防警报系统**——烟雾探测器检测到异常(状态变化),同时触发警铃、通知消防队、启动喷淋系统(多个观察者同时响应)。 ``` 没有观察者模式(直接依赖): ┌──────────┐ ┌─────────┐ │ 数据源 │────→│ 模块 A │ │ │────→│ 模块 B │ │ 知道所有 │────→│ 模块 C │ ← 紧耦合,N 个模块就 N 条硬连线 │ 消费者 │────→│ 模块 D │ └──────────┘ └─────────┘ 有观察者模式(解耦通知): ┌──────────┐ ┌──────────┐ ┌─────────┐ │ 数据源 │────→│ 通知机制 │────→│ 模块 A │ │ │ │ │────→│ 模块 B │ │ 不知道 │ │ 统一管理 │────→│ 模块 C │ ← 松耦合,动态增删 │ 谁在听 │ │ │────→│ 模块 D │ └──────────┘ └──────────┘ └─────────┘ ``` **观察者模式就是解决"一对多通知"问题的经典方案。** ------ ## 观察者模式详解 ### 模式定义 > **观察者模式(Observer Pattern)\**定义了对象之间的\**一对多依赖关系**,当一个对象(被观察者/主题)的状态发生变化时,所有依赖于它的对象(观察者)都会自动收到通知并更新。 观察者模式也被称为"发布-订阅模式"(Publish-Subscribe),虽然严格来说两者有细微区别(发布-订阅通常有一个中间消息代理),但核心思想一致:**解耦事件的产生和事件的处理**。 ### 核心角色 ``` ┌─────────────────────────────────────────────────────────┐ │ 观察者模式结构 │ │ │ │ ┌──────────────────┐ ┌──────────────────────┐ │ │ │ Subject │ │ Observer │ │ │ │ (被观察者) │ │ (观察者接口) │ │ │ │ │ │ │ │ │ │ + attach(obs) │◇────→│ + update(data) │ │ │ │ + detach(obs) │ └───────────┬───────────┘ │ │ │ + notify() │ │ │ │ └──────────────────┘ ┌───────┴────────┐ │ │ │ │ │ │ ┌───────────┴──┐ ┌──────────┴───┐ │ │ │ ConcreteObsA │ │ ConcreteObsB │ │ │ │ 显示模块 │ │ 日志模块 │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ attach = 订阅 detach = 取消订阅 notify = 通知 │ └─────────────────────────────────────────────────────────┘ ``` - • **Subject(被观察者/主题)**:维护一个观察者列表,提供注册、注销、通知的接口 - • **Observer(观察者接口)**:定义更新接口,所有具体观察者实现此接口 - • **ConcreteObserver(具体观察者)**:实现更新逻辑,定义对通知的具体响应 ### 基本实现(经典版本) 先看最经典的教科书式实现,理解核心思想: ``` #include #include #include #include // 前向声明 class Observer; // 被观察者基类 class Subject { public: virtual ~Subject() = default; void attach(Observer* observer) { observers_.push_back(observer); } void detach(Observer* observer) { observers_.erase( std::remove(observers_.begin(), observers_.end(), observer), observers_.end() ); } void notify(); // 定义在 Observer 声明之后 protected: std::vector observers_; }; // 观察者基类 class Observer { public: virtual ~Observer() = default; virtual void update(Subject* subject) = 0; }; // notify 的实现 void Subject::notify() { for (auto* observer : observers_) { observer->update(this); } } // ---- 具体实现 ---- class StockData : public Subject { public: void setPrice(const std::string& symbol, double price) { symbol_ = symbol; price_ = price; notify(); } const std::string& symbol() const { return symbol_; } double price() const { return price_; } private: std::string symbol_; double price_ = 0.0; }; class PriceDisplay : public Observer { public: void update(Subject* subject) override { auto* stock = dynamic_cast(subject); if (stock) { std::cout << "[显示面板] " << stock->symbol() << " 当前价格: " << stock->price() << std::endl; } } }; class PriceAlert : public Observer { public: explicit PriceAlert(double threshold) : threshold_(threshold) {} void update(Subject* subject) override { auto* stock = dynamic_cast(subject); if (stock && stock->price() > threshold_) { std::cout << "[价格警报] ⚠ " << stock->symbol() << " 价格 " << stock->price() << " 超过阈值 " << threshold_ << "!" << std::endl; } } private: double threshold_; }; class TradeLogger : public Observer { public: void update(Subject* subject) override { auto* stock = dynamic_cast(subject); if (stock) { std::cout << "[交易日志] 记录: " << stock->symbol() << " = " << stock->price() << std::endl; } } }; ``` **关键观察**: - • `StockData` 完全不知道 `PriceDisplay`、`PriceAlert`、`TradeLogger` 的存在 - • 运行时可以动态注册和注销观察者 - • 新增观察者类型不需要修改 `StockData` 的代码 ### 经典版本的问题 上面的实现虽然功能正确,但存在几个严重问题: **问题 1:裸指针的生命周期风险** ``` { PriceDisplay display; stock.attach(&display); } // display 被销毁! stock.setPrice("AAPL", 100.0); // 访问已销毁的对象 → 未定义行为! ``` **问题 2:dynamic_cast 带来类型安全和性能开销** ``` void update(Subject* subject) override { auto* stock = dynamic_cast(subject); // 运行时类型检查 if (stock) { /* ... */ } // 还要判空 } ``` **问题 3:接口不够灵活** 所有观察者必须继承 `Observer` 基类,无法直接使用 lambda 或普通函数作为回调。 接下来我们用现代 C++ 特性逐一解决这些问题。 ------ ## 推模型与拉模型 在观察者模式中,**数据如何从被观察者传递到观察者**,有两种经典策略。 ### 推模型(Push Model) 被观察者在通知时,**主动把变化的数据推送给观察者**: ``` class Observer { public: virtual ~Observer() = default; // 推模型:数据直接作为参数传递 virtual void onPriceChanged(const std::string& symbol, double price) = 0; }; class StockData { public: void setPrice(const std::string& symbol, double price) { symbol_ = symbol; price_ = price; for (auto* obs : observers_) { obs->onPriceChanged(symbol_, price_); // 把数据推给观察者 } } private: std::string symbol_; double price_; std::vector observers_; }; ``` **优点**: - • 观察者可以立刻拿到所需数据,不需要回头查询 - • 接口清晰,参数就是通知内容 **缺点**: - • 被观察者决定推什么数据,可能推了观察者不需要的信息 - • 如果数据很大(比如一个巨大的对象),每次通知都要拷贝或传引用 - • 新增推送字段时,所有观察者的接口都要改 ### 拉模型(Pull Model) 被观察者只通知"有变化了",**观察者自己决定要什么数据**: ``` class Observer { public: virtual ~Observer() = default; // 拉模型:只传被观察者本身,让观察者自己取数据 virtual void update(const StockData& source) = 0; }; class StockData { public: void setPrice(const std::string& symbol, double price) { symbol_ = symbol; price_ = price; for (auto* obs : observers_) { obs->update(*this); // 只通知"我变了",不推具体数据 } } // 提供数据访问接口 const std::string& symbol() const { return symbol_; } double price() const { return price_; } double change() const { return price_ - previousPrice_; } double volume() const { return volume_; } private: std::string symbol_; double price_; double previousPrice_; double volume_; std::vector observers_; }; ``` **优点**: - • 观察者按需获取数据,不浪费 - • 被观察者新增数据字段不影响现有观察者 - • 更灵活,观察者可以在 `update` 中根据需要查询不同数据 **缺点**: - • 观察者需要知道被观察者的接口,产生一定程度的耦合 - • 如果观察者需要的数据在通知时已经变了(多线程场景),可能拿到不一致的数据 ### 推拉结合(推荐做法) 实践中最常用的是**推拉结合**——推送必要的变化信息,观察者可以根据需要再查询更多: ``` // 事件数据(推送部分) struct PriceChangedEvent { std::string symbol; double oldPrice; double newPrice; double changePercent; }; class Observer { public: virtual ~Observer() = default; // 推送事件摘要,同时可以访问完整数据源 virtual void onPriceChanged(const PriceChangedEvent& event, const StockData& source) = 0; }; ``` 这种方式兼顾了推模型的便利和拉模型的灵活,是工程实践中的最佳平衡。 ------ ## 现代 C++ 实现 经典的继承式观察者模式存在接口僵硬、指针悬挂等问题。现代 C++ 提供了更好的工具。 ### 基于 std::function 的类型安全事件系统 `std::function` 可以封装任何可调用对象(函数指针、lambda、仿函数、成员函数),是实现观察者模式的利器: ``` #include #include #include #include #include template class Event { public: using HandlerId = size_t; using Handler = std::function; HandlerId subscribe(Handler handler) { HandlerId id = nextId_++; handlers_.push_back({id, std::move(handler)}); return id; } void unsubscribe(HandlerId id) { handlers_.erase( std::remove_if(handlers_.begin(), handlers_.end(), [id](constauto& entry) { return entry.id == id; }), handlers_.end() ); } void emit(Args... args) const { for (constauto& entry : handlers_) { entry.handler(args...); } } size_t subscriberCount() const { return handlers_.size(); } private: structHandlerEntry { HandlerId id; Handler handler; }; std::vector handlers_; HandlerId nextId_ = 0; }; ``` 使用起来非常简洁: ``` class StockData { public: void setPrice(const std::string& symbol, double price) { double oldPrice = price_; symbol_ = symbol; price_ = price; // 触发事件,所有订阅者自动收到通知 priceChanged.emit(symbol_, oldPrice, price_); if (price_ > 100.0) { highPriceAlert.emit(symbol_, price_); } } // 公开的事件——外部可以订阅 Event priceChanged; // symbol, oldPrice, newPrice Event highPriceAlert; // symbol, price private: std::string symbol_; double price_ = 0.0; }; ``` ### 生命周期安全的连接管理 `std::function` 版本解决了接口灵活性问题,但还没解决生命周期问题——如果观察者被销毁了,事件触发时会访问悬挂引用。 我们引入 `Connection` 和 `ScopedConnection` 来实现 RAII 风格的自动管理: ``` #include #include #include #include #include class Connection { public: Connection() = default; Connection(std::function disconnector) : disconnector_(std::make_shared>(std::move(disconnector))) {} void disconnect() { if (disconnector_ && *disconnector_) { (*disconnector_)(); *disconnector_ = nullptr; } } bool connected() const { return disconnector_ && *disconnector_ != nullptr; } private: std::shared_ptr> disconnector_; }; // RAII 连接——作用域结束自动断开 class ScopedConnection { public: ScopedConnection() = default; explicit ScopedConnection(Connection conn) : conn_(std::move(conn)) {} ~ScopedConnection() { conn_.disconnect(); } ScopedConnection(const ScopedConnection&) = delete; ScopedConnection& operator=(const ScopedConnection&) = delete; ScopedConnection(ScopedConnection&&) = default; ScopedConnection& operator=(ScopedConnection&&) = default; void disconnect() { conn_.disconnect(); } bool connected() const { return conn_.connected(); } private: Connection conn_; }; template class Signal { public: using Handler = std::function; using HandlerId = size_t; Connection connect(Handler handler) { HandlerId id = nextId_++; handlers_.push_back({id, std::move(handler)}); // 返回一个 Connection,持有断开逻辑 returnConnection([this, id]() { this->disconnect(id); }); } void emit(Args... args) const { // 拷贝一份,防止回调中修改 handlers_ 导致迭代器失效 auto handlersCopy = handlers_; for (constauto& entry : handlersCopy) { entry.handler(args...); } } size_t slotCount() const { return handlers_.size(); } private: void disconnect(HandlerId id) { handlers_.erase( std::remove_if(handlers_.begin(), handlers_.end(), [id](constauto& entry) { return entry.id == id; }), handlers_.end() ); } structHandlerEntry { HandlerId id; Handler handler; }; std::vector handlers_; HandlerId nextId_ = 0; }; ``` RAII 连接的威力在于:观察者被销毁时,连接自动断开,永远不会出现悬挂指针: ``` class StockMonitor { public: StockMonitor(const std::string& name, Signal& signal) : name_(name) { // ScopedConnection 绑定到成员变量,对象销毁时自动断开 connection_ = ScopedConnection( signal.connect([this](const std::string& symbol, double price) { std::cout << "[" << name_ << "] " << symbol << " = " << price << std::endl; }) ); } private: std::string name_; ScopedConnection connection_; }; ``` ### 线程安全的事件系统 在多线程环境中,事件的订阅、取消和触发可能并发执行,需要线程安全保护: ``` #include #include #include #include #include template class ThreadSafeSignal { public: using Handler = std::function; using HandlerId = size_t; Connection connect(Handler handler) { std::unique_lock lock(mutex_); HandlerId id = nextId_++; handlers_.push_back({id, std::move(handler)}); returnConnection([this, id]() { this->disconnect(id); }); } void emit(Args... args) const { std::vector snapshot; { std::shared_lock lock(mutex_); snapshot = handlers_; // 拷贝快照 } // 在锁外调用回调,避免死锁 for (constauto& entry : snapshot) { entry.handler(args...); } } size_t slotCount() const { std::shared_lock lock(mutex_); return handlers_.size(); } private: void disconnect(HandlerId id) { std::unique_lock lock(mutex_); handlers_.erase( std::remove_if(handlers_.begin(), handlers_.end(), [id](constauto& entry) { return entry.id == id; }), handlers_.end() ); } structHandlerEntry { HandlerId id; Handler handler; }; mutable std::shared_mutex mutex_; std::vector handlers_; HandlerId nextId_ = 0; }; ``` **关键设计决策**: ``` 为什么 emit() 要先拷贝快照再调用? 1. 如果持有锁调用回调,回调中如果又订阅/取消订阅,就会死锁 2. 拷贝快照后释放锁,回调可以安全地修改订阅列表 3. 代价是一次 vector 拷贝,但保证了安全性 ┌──────────┐ │ emit() │ │ │ │ ① 加锁 │ │ ② 拷贝 │ ← 持锁时间很短 │ ③ 解锁 │ │ │ │ ④ 遍历 │ ← 无锁状态调用回调 │ 快照 │ 回调中可以安全地 connect/disconnect └──────────┘ ``` ------ ## 观察者模式与设计原则 ### 开闭原则(Open-Closed Principle) 观察者模式完美体现了"对扩展开放,对修改封闭"的原则: ``` 新增一个观察者(比如"手机推送"功能): ✅ 创建新的 MobilePushView 类 ✅ 在外部连接到 Model 的信号 ❌ 不需要修改 Model 的任何代码 model.taskAdded.connect([](const Task& task) { // 发送手机推送通知 pushToMobile(task.title); }); ``` ### 依赖倒置原则(Dependency Inversion Principle) ``` 传统做法:高层依赖低层 StockData → PriceDisplay (StockData 依赖具体的 PriceDisplay 类) 观察者模式:都依赖抽象 StockData → Signal<...> (只依赖 Signal 接口) PriceDisplay → Signal<...> (只依赖 Signal 接口) ``` ### 单一职责原则(Single Responsibility Principle) - • **Subject 的职责**:管理状态 + 发出通知(不关心谁在听) - • **Observer 的职责**:响应通知(不关心通知从哪来) - • **Signal 的职责**:管理订阅关系(纯粹的通信机制) ------ ## 常见陷阱与最佳实践 ### 陷阱 1:回调中修改订阅列表导致迭代器失效 ``` // 危险!回调中取消订阅自己 signal.connect([&signal, &conn](int value) { std::cout << "收到: " << value << std::endl; conn.disconnect(); // 在遍历过程中修改了 handlers_! }); ``` **解决**:在 `emit()` 中先拷贝快照再遍历(前文的实现已经处理了这一点)。 ### 陷阱 2:观察者销毁后仍被通知(悬挂引用) ``` Signal signal; { MyObserver obs; signal.connect([&obs](int v) { obs.handle(v); }); } // obs 销毁了 signal.emit(42); // 访问已销毁的 obs → 崩溃! ``` **解决**:使用 `ScopedConnection`,在观察者析构时自动断开连接。 ### 陷阱 3:通知风暴(Notification Storm) ``` // A 观察 B,B 观察 A → 无限循环! a.valueChanged.connect([&b](int v) { b.setValue(v); }); b.valueChanged.connect([&a](int v) { a.setValue(v); }); a.setValue(1); // → 通知 b → b 通知 a → a 通知 b → ... ``` **解决**:在 setter 中检查值是否真的变化了: ``` void setValue(int value) { if (value != value_) { // 只在值真正变化时才通知 value_ = value; valueChanged.emit(value_); } } ``` ### 陷阱 4:通知顺序依赖 ``` // 不要依赖观察者被通知的顺序 // 观察者 A 和 B 都订阅了同一个信号 // A 先收到还是 B 先收到?不要假设! ``` **最佳实践**:每个观察者的响应应该是独立的,不依赖其他观察者的执行顺序。 ### 陷阱 5:在析构函数中发射信号 ``` class Widget { public: ~Widget() { destroyed.emit(); // 谨慎!订阅者可能访问已部分销毁的对象 } Signal<> destroyed; }; ``` **最佳实践**:如果需要"销毁通知",确保在成员变量被销毁之前发射,并且订阅者不要访问即将销毁的对象的其他成员。 ### 最佳实践总结 | 实践 | 说明 | | ------------------------- | ---------------------------------------------- | | **使用 ScopedConnection** | 让订阅跟随对象生命周期,避免悬挂引用 | | **emit 前拷贝快照** | 防止回调中修改订阅列表导致迭代器失效 | | **值变化检查** | setter 中只在值真的变了才通知,防止通知风暴 | | **避免循环依赖** | 设计时注意 A↔B 互相观察的情况 | | **不依赖通知顺序** | 每个观察者应独立处理通知 | | **多线程用读写锁** | 订阅/取消用写锁,通知用读锁+快照 | | **事件粒度适中** | 太细碎的事件导致频繁通知,太粗粒度导致无用更新 | ------ ## 何时使用观察者模式 ### 适用场景 - • **事件驱动系统**:GUI 事件、用户交互、快捷键绑定 - • **数据绑定**:Model 变化自动更新 View(MVC/MVVM) - • **消息通知**:邮件系统、推送通知、告警系统 - • **松耦合通信**:模块间通信不想直接引用 - • **日志/审计**:在不修改业务代码的前提下插入日志记录 ### 不适用场景 - • **一对一固定关系**:只有一个固定的响应者,直接调用更简单 - • **严格的顺序要求**:需要观察者按特定顺序执行 - • **同步要求**:需要知道所有观察者都处理完了才能继续 - • **高性能热路径**:每秒数百万次的通知,`std::function` 开销可能不可接受 ------ ## 写在最后 观察者模式是行为型模式中最重要、应用最广泛的模式之一。它的核心价值在于:**在不修改数据源的前提下,通过订阅-通知机制实现一对多的松耦合通信**。 在 C++ 中,观察者模式的实现经历了从经典到现代的演进: - • **经典实现**用继承和裸指针,简单直观但存在生命周期风险 - • **现代实现**用 `std::function` + `Signal`,类型安全、lambda 友好、灵活强大 - • **RAII 实现**用 `ScopedConnection`,让订阅跟随对象生命周期自动管理 - • **线程安全实现**用 `shared_mutex` + 快照遍历,保证多线程场景下的正确性 ``` 实现演进路线: 经典继承 → std::function → ScopedConnection → ThreadSafeSignal ↓ ↓ ↓ ↓ 简单直观 灵活解耦 生命周期安全 多线程安全 ``` **记住这三句话**: > **不要打电话给我,我会打给你(Don't call us, we'll call you)——这就是观察者模式的核心哲学。** > **让数据的产生者不再操心"谁需要这些数据"——这就是松耦合的力量。** > **ScopedConnection 是 C++ 观察者模式的安全带——没有它,随时可能车毁人亡。**