
1. 项目概述为什么我们需要观察者模式在C项目里摸爬滚打十几年我见过太多因为对象间通信混乱而导致的“面条式”代码。想象一下你正在开发一个股票交易监控系统。当某只股票的价格发生变动时你需要实时更新用户界面的图表、刷新持仓列表、触发风险预警、并记录交易日志。最直接的做法是什么你可能会在Stock类的setPrice()方法里写上一大串调用void Stock::setPrice(double newPrice) { this-price newPrice; // 更新UI uiChart.update(this-symbol, newPrice); // 刷新持仓 portfolio.refresh(this); // 检查风险 riskMonitor.check(this); // 记录日志 logger.logPriceChange(this-symbol, newPrice); // ... 未来再加个短信通知还得回来改这里 }这段代码的问题显而易见紧耦合。Stock类发布者必须知道所有依赖它的对象订阅者的具体类型和接口。每增加一个新的关注点比如短信通知你就必须修改Stock的核心业务逻辑这严重违反了开闭原则对扩展开放对修改关闭。更糟糕的是如果uiChart对象暂时不可用整个价格设置流程都可能崩溃。这就是观察者模式Observer Pattern或者说发布-订阅机制Pub-Sub要解决的核心问题建立一种一对多的依赖关系当一个对象发布者的状态发生改变时所有依赖于它的对象订阅者都能自动得到通知并更新且发布者无需知道订阅者的具体细节。在C中实现它不仅仅是套用一个设计模式更是对对象间松耦合通信机制的一次深刻实践。它让我们的系统更像一个灵活的事件驱动网络而不是一个僵化的调用链。2. 核心设计思路从“通知所有人”到“事件总线”观察者模式的结构非常经典但用C实现时我们需要仔细考量一些语言特性带来的独特挑战和机遇比如内存管理、线程安全和性能。2.1 模式结构拆解模式的核心角色通常被定义为四个Subject目标/发布者维护一个观察者列表提供添加attach和删除detach观察者的接口并在状态改变时调用notify方法。Observer观察者/订阅者定义一个更新接口通常是update()供发布者在通知时调用。ConcreteSubject具体发布者继承或实现Subject存储自身状态。当状态改变时向所有注册的观察者发出通知。ConcreteObserver具体观察者实现Observer接口维护一个对ConcreteSubject的引用或通过其他方式获取状态并在update被调用时同步自身状态。在C的语境下一个更贴近现代实践的思路是将Subject中管理列表和通知的逻辑抽离出来形成一个独立的事件管理器EventManager或消息频道Channel。这样具体的业务对象如Stock可以更纯粹只需持有这个管理器并委托它去发布事件。2.2 C实现的独特考量为什么用C实现观察者模式需要格外小心主要因为以下几点内存管理这是C开发者永远的课题。观察者列表里存储的是指针还是智能指针如果是原始指针发布者是否需要负责观察者的生命周期一个常见的坑是观察者对象已经delete了但忘记从发布者的列表中移除导致后续通知时发生悬空指针访问程序崩溃。我的经验是在大多数情况下使用std::weak_ptr来存储观察者引用是最安全的选择。它允许你检查观察者是否还存在避免了悬空指针又不会像std::shared_ptr那样影响对象的生命周期。线程安全你的系统是多线程的吗如果是那么attach、detach和notify操作都可能是并发执行的。在遍历观察者列表进行通知时如果另一个线程正在修改这个列表添加或删除观察者就会导致迭代器失效引发未定义行为。一个实用的做法是使用std::mutex对观察者列表的访问进行加锁。但要注意在notify函数内锁的粒度要小心控制最好先复制一份观察者列表的副本然后对副本进行通知这样可以缩短锁的持有时间并避免在调用观察者update方法时该方法可能执行很长时间或再次尝试获取锁仍持有列表锁导致死锁。性能与拷贝开销通知时往往需要传递一些事件数据。是传递值、引用还是指针如果数据很大传递值拷贝开销大传递引用或指针则要确保数据在观察者使用期间有效。通常我会定义一个轻量级的EventData结构体包含事件类型和必要的上下文信息如std::any或类型擦除的包装器然后通过const或智能指针传递。2.3 接口设计与事件类型化经典的观察者模式接口很简单virtual void update() 0;。但在实际项目中这往往不够。不同的观察者可能只关心特定类型的事件。比如日志观察者关心所有事件而UI图表观察者只关心“价格更新”事件。因此一个更强大的设计是引入**事件类型Event Type**的概念。发布者可以发布不同类型的事件如EVENT_PRICE_CHANGEEVENT_VOLUME_CHANGE而观察者在订阅时指定自己感兴趣的事件类型。这样notify的时候只会通知订阅了该事件类型的观察者减少了不必要的函数调用。// 事件类型枚举 enum class EventType { PriceChanged, VolumeChanged, TradeCompleted }; // 订阅时指定类型 eventManager.subscribe(EventType::PriceChanged, observerPtr);3. 从零到一一个可复用的C观察者模式实现理论说再多不如一行代码。下面我将构建一个线程安全、支持事件类型、内存管理清晰的通用观察者模式框架。这个实现可以直接用到你的项目中。3.1 定义核心接口与事件数据首先我们定义观察者接口和事件数据。为了让事件数据能携带任意类型的信息我们使用std::anyC17或自行实现一个简单的类型擦除包装器。// Observer.h #pragma once #include memory #include any #include string // 前向声明 class EventData; // 观察者接口 class IObserver { public: virtual ~IObserver() default; // 更新方法接收事件数据和发布者可选的指针 virtual void update(const std::string eventType, const EventData data) 0; }; // 简单的事件数据包装类 class EventData { public: EventData() default; templatetypename T EventData(T value) : data_(std::forwardT(value)) {} templatetypename T T get() const { return std::any_castT(data_); } bool hasValue() const { return data_.has_value(); } private: std::any data_; };3.2 实现事件管理器发布者核心这是最核心的类负责管理订阅关系和派发事件。我们使用std::unordered_map来按事件类型组织观察者列表并使用std::weak_ptr来避免生命周期问题。// EventManager.h #pragma once #include “Observer.h” #include unordered_map #include vector #include memory #include mutex #include string class EventManager { public: using ObserverPtr std::weak_ptrIObserver; using ObserverList std::vectorObserverPtr; // 订阅事件 void subscribe(const std::string eventType, ObserverPtr observer) { std::lock_guardstd::mutex lock(mutex_); observersMap_[eventType].push_back(observer); } // 取消订阅需要精确匹配观察者对象 void unsubscribe(const std::string eventType, ObserverPtr observer) { std::lock_guardstd::mutex lock(mutex_); auto it observersMap_.find(eventType); if (it ! observersMap_.end()) { auto list it-second; // 使用 std::remove_if 和 erase 移除特定的 weak_ptr // 注意比较 weak_ptr 需要先 lock() 成 shared_ptr 再比较 list.erase(std::remove_if(list.begin(), list.end(), [observer](const ObserverPtr wp) { auto sp1 wp.lock(); auto sp2 observer.lock(); return sp1 sp2 sp1.get() sp2.get(); }), list.end()); } } // 通知所有订阅了某事件类型的观察者 void notify(const std::string eventType, const EventData data) { ObserverList observersToNotify; { // 1. 加锁复制观察者列表快照 std::lock_guardstd::mutex lock(mutex_); auto it observersMap_.find(eventType); if (it ! observersMap_.end()) { observersToNotify it-second; // 复制 } } // 2. 锁的作用域结束释放锁 // 3. 遍历快照进行通知此时已不持有锁安全且避免死锁 for (auto weakObserver : observersToNotify) { if (auto observer weakObserver.lock()) { // 检查对象是否还存在 observer-update(eventType, data); } } // 可选清理列表中已失效的 weak_ptr cleanupExpiredObservers(eventType); } private: void cleanupExpiredObservers(const std::string eventType) { std::lock_guardstd::mutex lock(mutex_); auto it observersMap_.find(eventType); if (it ! observersMap_.end()) { auto list it-second; list.erase(std::remove_if(list.begin(), list.end(), [](const ObserverPtr wp) { return wp.expired(); // 检查 weak_ptr 是否已失效 }), list.end()); } } std::unordered_mapstd::string, ObserverList observersMap_; mutable std::mutex mutex_; // mutable 允许在 const 成员函数中加锁 };关键点解析std::weak_ptr的使用观察者列表存储的是weak_ptr这表示事件管理器并不“拥有”观察者。观察者的生命周期由其他地方如智能指针管理。这解耦了生命周期。锁与快照notify函数中我们先在锁的保护下复制快照当前观察者列表然后立即释放锁再遍历快照进行通知。这是保证线程安全且避免死锁的经典做法。因为observer-update()可能执行任意代码甚至可能回调subscribe或unsubscribe如果此时还持有列表锁就会导致死锁。失效清理cleanupExpiredObservers函数会定期或在通知后清理那些已经失效的weak_ptr防止列表无限膨胀。3.3 具体业务类的实现现在让我们用股票和观察者的例子来演示如何使用这个框架。// Stock.h (具体发布者) #pragma once #include “EventManager.h” #include string class Stock { public: Stock(const std::string symbol, double price) : symbol_(symbol), price_(price) {} void setPrice(double newPrice) { if (std::abs(newPrice - price_) 1e-6) { // 简单判断价格是否真有变化 double oldPrice price_; price_ newPrice; // 准备事件数据 EventData data; // 可以传递一个结构体这里简单传递新旧价格 struct PriceChangeEvent { std::string symbol; double oldPrice; double newPrice; }; data PriceChangeEvent{symbol_, oldPrice, newPrice}; // 发布事件 eventManager_.notify(“PriceChanged”, data); // 可以发布更多事件类型比如一个通用的“StockUpdated” eventManager_.notify(“StockUpdated”, EventData{symbol_}); } } EventManager getEventManager() { return eventManager_; } const std::string getSymbol() const { return symbol_; } double getPrice() const { return price_; } private: std::string symbol_; double price_; EventManager eventManager_; // 每个Stock对象拥有自己的事件管理器 }; // PriceDisplay.h (具体观察者) #pragma once #include “Observer.h” #include iostream #include memory class PriceDisplay : public IObserver, public std::enable_shared_from_thisPriceDisplay { public: PriceDisplay(const std::string name) : name_(name) {} void update(const std::string eventType, const EventData data) override { if (eventType “PriceChanged”) { try { auto event data.getStock::PriceChangeEvent(); // 假设EventData里存的是这个结构体 std::cout “[“ name_ “] 价格更新: ” event.symbol “ ” event.oldPrice “ - ” event.newPrice std::endl; } catch (const std::bad_any_cast e) { std::cerr “PriceDisplay 接收到 PriceChanged 事件但数据格式错误!” std::endl; } } // 可以处理其他事件类型... } // 提供一个便捷的订阅方法 void subscribeTo(std::shared_ptrStock stock) { stock-getEventManager().subscribe(“PriceChanged”, weak_from_this()); } private: std::string name_; };3.4 客户端代码与运行示例最后看看如何将它们组装起来。// main.cpp #include “Stock.h” #include “PriceDisplay.h” #include memory int main() { // 创建发布者 auto appleStock std::make_sharedStock(“AAPL”, 150.0); // 创建观察者 auto display1 std::make_sharedPriceDisplay(“终端1”); auto display2 std::make_sharedPriceDisplay(“终端2”); auto logger std::make_sharedLoggingObserver(); // 假设有另一个日志观察者 // 建立订阅关系 display1-subscribeTo(appleStock); display2-subscribeTo(appleStock); appleStock-getEventManager().subscribe(“StockUpdated”, logger); std::cout “初始订阅完成。“ std::endl; // 触发状态改变 appleStock-setPrice(155.5); // 两个Display和Logger都会收到通知 appleStock-setPrice(155.5); // 价格未变不会触发通知 appleStock-setPrice(160.0); // 再次触发通知 // 动态取消订阅 appleStock-getEventManager().unsubscribe(“PriceChanged”, display1); std::cout “\nDisplay1 已取消订阅 PriceChanged 事件。“ std::endl; appleStock-setPrice(158.0); // 只有display2和logger会收到通知 return 0; }4. 高级话题与性能优化实现了一个基础版本后我们可以探讨一些进阶话题让你的观察者模式更加强大和高效。4.1 使用std::function与信号槽Signals and Slots如果你觉得为每一个观察行为都创建一个类太重量级C11的std::function和std::bind或lambda表达式提供了轻量级的替代方案。你可以让事件管理器直接存储std::function对象。// 使用 std::function 的事件管理器 class FunctionalEventManager { public: using Callback std::functionvoid(const EventData); using CallbackList std::vectorCallback; void subscribe(const std::string eventType, Callback cb) { std::lock_guardstd::mutex lock(mutex_); callbacksMap_[eventType].push_back(std::move(cb)); } void notify(const std::string eventType, const EventData data) { CallbackList callbacks; { std::lock_guardstd::mutex lock(mutex_); auto it callbacksMap_.find(eventType); if (it ! callbacksMap_.end()) { callbacks it-second; } } for (const auto cb : callbacks) { if (cb) cb(data); } } // ... 其他类似 };使用起来更加灵活FunctionalEventManager mgr; mgr.subscribe(“Event”, [](const EventData d) { std::cout “Lambda 处理事件!” std::endl; }); // 或者绑定成员函数 SomeClass obj; mgr.subscribe(“Event”, std::bind(SomeClass::handler, obj, std::placeholders::_1));这就是Qt框架中信号槽机制的简化版核心思想。它的优点是极其灵活缺点是std::function的调用比虚函数update()略慢通常可忽略且失去了观察者对象的显式身份取消订阅需要保存好std::function对象比较麻烦。4.2 异步通知与事件队列在notify中同步调用所有观察者的update方法有一个潜在问题如果某个观察者的处理非常耗时它会阻塞当前线程导致发布者和其他观察者等待。在高性能或实时系统中这是不可接受的。解决方案是引入异步事件队列。发布者notify时并不直接调用观察者而是将一个事件对象包含事件类型、数据和回调函数放入一个线程安全的队列中。由单独的一个或多个工作线程从这个队列中取出事件并执行真正的通知逻辑。class AsyncEventManager { // ... 拥有一个 EventQueue std::thread workerThread_; std::atomicbool running_{true}; public: AsyncEventManager() { workerThread_ std::thread([this]() { while (running_) { EventItem item; if (queue_.pop(item, std::chrono::milliseconds(100))) { // 超时等待 // 在实际项目中这里应该从 map 里找到对应的 callback 列表 for (auto cb : getCallbacksFor(item.eventType)) { cb(item.data); } } } }); } ~AsyncEventManager() { running_ false; if (workerThread_.joinable()) workerThread_.join(); } void notifyAsync(const std::string eventType, EventData data) { queue_.push({eventType, std::move(data)}); } };这实现了发布者与观察者处理逻辑的解耦提升了系统的响应性和吞吐量。当然复杂度也大大增加你需要考虑事件顺序、线程池、任务窃取等问题。4.3 观察者模式的变体与相关模式推模型 vs 拉模型我们实现的是“推模型”即发布者将数据作为参数推送给观察者。还有“拉模型”即发布者只通知“有变化”观察者收到通知后主动从发布者那里“拉取”所需数据。拉模型给了观察者更多灵活性但增加了耦合观察者需要知道如何获取数据。与中介者模式的关系当系统中对象间通信非常复杂形成网状结构时观察者模式可能使关系混乱。此时可以引入一个中介者Mediator作为所有通信的中心枢纽。对象将事件发送给中介者由中介者负责路由给正确的接收者。你可以把中介者看作一个全局的、更复杂的事件管理器。一个简单的判断准则是如果对象之间是直接的一对多通知关系用观察者如果是多对多、通信逻辑复杂考虑中介者。5. 实战避坑指南与性能调优纸上得来终觉浅绝知此事要躬行。下面是我在多年项目实践中总结的几个关键陷阱和优化建议。5.1 内存泄漏与循环引用这是使用智能指针时最容易掉进去的坑。场景ConcreteSubject持有std::shared_ptrIObserver而ConcreteObserver也持有std::shared_ptrConcreteSubject为了在update中获取状态。这就形成了循环引用导致两者都无法被释放。解决方案使用std::weak_ptr打破循环在观察者中使用std::weak_ptrSubject来引用发布者。需要时通过lock()获取临时shared_ptr。明确生命周期所有权仔细思考谁应该拥有对象的所有权。通常发布者的生命周期独立于观察者。观察者列表应使用weak_ptr正如我们基础实现中所做的那样。在析构函数中自动取消订阅在ConcreteObserver的析构函数中调用subject-detach(this)。但这要求观察者必须知道它订阅了谁并且发布者必须提供通过原始指针取消订阅的方法因为this是原始指针。使用weak_ptr方案可以更优雅地避免这个问题。5.2 通知过程中的增删观察者如果在观察者A的update方法中它取消了观察者B的订阅或者订阅了新的观察者C而此刻发布者正在遍历观察者列表进行通知会发生什么问题迭代器失效。在C中在遍历容器如vector时修改容器增删元素是未定义行为。解决方案我们之前实现的快照Snapshot方法完美解决了这个问题。因为通知遍历的是列表的副本原列表的修改不会影响本次通知。这也是为什么在notify函数中要先复制再遍历的原因。5.3 性能瓶颈分析与优化当观察者数量巨大成千上万或者事件触发非常频繁时简单的实现可能成为瓶颈。瓶颈1锁竞争。每次subscribe、unsubscribe、notify都要抢同一把锁mutex_。优化使用读写锁std::shared_mutexC17。notify读操作可以共享访问而subscribe/unsubscribe写操作需要独占访问。这能显著提升高并发读场景的性能。瓶颈2线性遍历。notify时需要线性遍历所有订阅了某事件的观察者O(n)复杂度。优化对于超大规模场景可以考虑按优先级分组或者使用更高效的数据结构。但绝大多数情况下线性遍历已经足够快因为事件处理本身update函数的耗时通常远大于遍历开销。瓶颈3std::any的类型转换开销。std::any_cast涉及运行时类型检查RTTI有一定开销。优化如果事件类型固定且不多可以放弃通用性使用std::variantC17或手工定义的联合体union来存储事件数据通过std::visit来访问性能更好类型安全。5.4 设计模式不是银弹最后也是最重要的心得不要为了用模式而用模式。观察者模式引入了间接性使得代码的流程不像直接调用那么一目了然。调试时你需要追踪事件的发布和订阅链。对于简单的、一对一的回调直接使用函数指针、std::function或者简单的接口回调可能更清晰。只有当对象间确实存在稳定的一对多依赖关系且你希望降低耦合、支持动态订阅时观察者模式才是最佳选择。在我参与的一个大型实时数据分发系统中观察者模式是架构的基石。它让数据源发布者和各式各样的数据处理器、显示器、存储器观察者完全解耦。新的处理模块可以随时插入旧的可以随时移除而核心数据流代码数年未曾改动。这种灵活性和可维护性正是精心实现的设计模式所带来的长期价值。