
C++观察者模式实战应用:从理论到生产级实现
你好,我是源码库的博主。今天想和你深入聊聊C++中的观察者模式。这个模式我几乎在每个大型项目里都用过,从GUI事件处理到游戏引擎的消息系统,再到微服务间的状态同步,它无处不在。但说实话,第一次实现时我也踩了不少坑——内存泄漏、线程安全、循环依赖,该遇到的“惊喜”一个没少。所以这篇文章,我想结合这些实战经验,带你从零构建一个生产可用的观察者模式实现,而不仅仅是教科书上的“Hello World”。
一、为什么我们需要观察者模式?
让我从一个真实场景说起。去年我负责一个实时数据监控系统,需要将同一份市场行情数据同时推送给日志模块、风控模块和UI显示模块。最初我用最直接的方式:在数据接收处硬编码调用这三个模块的接口。结果呢?每加一个新模块(比如后来需要的报警模块),我就得修改核心数据接收代码,单元测试全部重跑,还差点因为一次紧急修改引入了死循环。这种紧耦合的设计,简直就是维护的噩梦。
观察者模式正是为了解决这种一对多的依赖关系而生的。它的核心思想很简单:让主题(Subject)维护一个观察者(Observer)列表,当主题状态变化时,自动通知所有观察者,而主题本身不需要知道观察者的具体细节。这完美符合了开放-封闭原则——对扩展开放,对修改封闭。
二、基础实现:先让它跑起来
我们先从最经典的实现开始。这里我建议你打开编辑器跟着敲一遍,理解会更深刻。
// Observer.hpp - 观察者接口
class Observer {
public:
virtual ~Observer() = default;
virtual void update(const std::string& message) = 0;
};
// Subject.hpp - 主题接口
class Subject {
public:
virtual ~Subject() = default;
virtual void attach(Observer* observer) = 0;
virtual void detach(Observer* observer) = 0;
virtual void notify(const std::string& message) = 0;
};
// ConcreteSubject.hpp - 具体主题
class ConcreteSubject : public Subject {
private:
std::vector observers_;
public:
void attach(Observer* observer) override {
observers_.push_back(observer);
}
void detach(Observer* observer) override {
observers_.erase(
std::remove(observers_.begin(), observers_.end(), observer),
observers_.end()
);
}
void notify(const std::string& message) override {
for (auto* observer : observers_) {
observer->update(message);
}
}
// 业务方法:价格更新
void setPrice(double price) {
price_ = price;
notify("Price updated to: " + std::to_string(price));
}
private:
double price_;
};
这个基础版本能工作,但在生产环境中直接使用会出问题。我踩过的第一个坑:裸指针管理。如果观察者先于主题被销毁,主题持有的就是野指针,notify时必然崩溃。
三、进阶实现:解决内存与线程安全问题
基于实战教训,我们来构建一个更健壮的版本。这里有几个关键改进点:
// SafeObserver.hpp - 使用智能指针的增强版
#include
#include
#include
class SafeObserver : public std::enable_shared_from_this {
public:
virtual ~SafeObserver() = default;
virtual void update(const std::string& message) = 0;
// 提供弱指针供主题持有
std::weak_ptr getWeakPtr() {
return weak_from_this();
}
};
class ThreadSafeSubject {
private:
std::vector<std::weak_ptr> observers_;
mutable std::mutex mutex_; // mutable允许在const方法中加锁
public:
void attach(std::shared_ptr observer) {
std::lock_guard lock(mutex_);
observers_.push_back(observer->getWeakPtr());
}
void detach(std::shared_ptr observer) {
std::lock_guard lock(mutex_);
auto it = std::remove_if(observers_.begin(), observers_.end(),
[&](const std::weak_ptr& wp) {
auto sp = wp.lock();
return !sp || sp == observer;
});
observers_.erase(it, observers_.end());
}
void notify(const std::string& message) {
std::vector<std::shared_ptr> validObservers;
{
std::lock_guard lock(mutex_);
// 清理过期观察者并收集有效观察者
auto it = std::remove_if(observers_.begin(), observers_.end(),
[&](std::weak_ptr& wp) {
return wp.expired();
});
observers_.erase(it, observers_.end());
// 锁定弱指针
for (auto& wp : observers_) {
if (auto sp = wp.lock()) {
validObservers.push_back(sp);
}
}
}
// 在锁外调用update,避免死锁和性能问题
for (auto& observer : validObservers) {
observer->update(message);
}
}
};
这个版本解决了几个关键问题:
- 使用weak_ptr避免循环引用:主题持有观察者的弱引用,不影响其生命周期
- 自动清理过期观察者:在notify时自动移除已被销毁的观察者
- 线程安全:使用互斥锁保护观察者列表,但注意update调用在锁外执行
四、实战技巧:模板化与性能优化
在实际项目中,我们经常需要传递不同类型的数据。硬编码消息类型会限制扩展性。下面是我在交易系统中使用的模板化方案:
// 事件基类
struct Event {
virtual ~Event() = default;
virtual std::string type() const = 0;
};
// 模板化观察者
template
class TypedObserver {
public:
virtual ~TypedObserver() = default;
virtual void onEvent(const EventType& event) = 0;
};
// 类型安全的主题
class EventBus {
private:
// 类型擦除存储
class ObserverWrapper {
public:
virtual ~ObserverWrapper() = default;
virtual void notify(const Event& event) = 0;
};
template
class TypedObserverWrapper : public ObserverWrapper {
TypedObserver* observer_;
public:
TypedObserverWrapper(TypedObserver* observer)
: observer_(observer) {}
void notify(const Event& event) override {
if (event.type() == EventType::staticType()) {
observer_->onEvent(static_cast(event));
}
}
};
std::unordered_map<std::string, std::vector<std::unique_ptr>> observers_;
std::mutex mutex_;
public:
template
void subscribe(TypedObserver* observer) {
std::lock_guard lock(mutex_);
observers_[EventType::staticType()].push_back(
std::make_unique<TypedObserverWrapper>(observer)
);
}
void publish(const Event& event) {
std::vector toNotify;
{
std::lock_guard lock(mutex_);
auto it = observers_.find(event.type());
if (it != observers_.end()) {
for (auto& wrapper : it->second) {
toNotify.push_back(wrapper.get());
}
}
}
for (auto* wrapper : toNotify) {
wrapper->notify(event);
}
}
};
这个设计的好处是类型安全且高效。每个事件类型有独立的观察者列表,发布时只需查找对应类型,避免了遍历所有观察者。
五、避坑指南:我踩过的那些坑
1. 在update中修改观察者列表:曾经有观察者在update方法中detach自己,导致迭代器失效崩溃。解决方案:像上面那样,先复制列表再通知。
2. update抛出异常:一个观察者的异常会影响其他观察者。建议:
try {
observer->update(message);
} catch (...) {
// 记录日志,但继续通知其他观察者
logger.error("Observer update failed");
}
3. 性能瓶颈:高频事件(如鼠标移动)可能产生大量通知。解决方案:批量处理或使用异步通知队列。
4. 循环通知:A通知B,B又通知A,形成死循环。一定要在设计时理清依赖关系。
六、现代C++的优雅实现(C++17+)
如果你能用C++17或更高版本,可以写出更简洁的代码:
template
class Signal {
using Slot = std::function;
std::vector<std::weak_ptr> slots_;
public:
auto connect() -> std::shared_ptr {
auto slot = std::make_shared();
slots_.emplace_back(slot);
return slot;
}
void emit(Args... args) {
// 使用结构化绑定和if初始化
for (auto it = slots_.begin(); it != slots_.end(); ) {
if (auto slot = it->lock()) {
(*slot)(args...);
++it;
} else {
it = slots_.erase(it);
}
}
}
};
// 使用示例
Signal dataSignal;
auto connection = dataSignal.connect();
*connection = [](int id, const std::string& name) {
std::cout << "Received: " << id << ", " << name << std::endl;
};
dataSignal.emit(42, "Answer");
七、实际应用场景
在我的项目中,观察者模式主要用在:
- 配置热更新:配置文件修改后,自动通知所有使用配置的模块
- 游戏引擎:实体组件系统(ECS)中的事件系统
- 交易系统:行情变化时通知策略引擎、风控、日志等模块
- UI框架:按钮点击、数据变化等事件处理
观察者模式是解耦的利器,但也要避免滥用。如果观察者链过长或关系复杂,考虑使用更专门的消息总线(Message Bus)或响应式编程库。
最后给个建议:在项目初期就设计好事件/消息系统的基础设施,后期重构成本会很高。希望我的这些经验能帮你少走弯路。如果有具体问题,欢迎在源码库社区交流讨论!

评论(0)