想象一下,你是一名C++程序员,负责开发公司核心的股票交易系统。系统的核心是一个Stock类,它存储股票的实时价格:
class Stock {
private:
std::string symbol_;
double price_;
public:
void setPrice(double price){ price_ = price; }
double getPrice() const{ return price_; }
};
现在有三个模块需要关注股价变化:交易界面要显示最新价格;价格超过阈值时预警模块要发出警报;每次价格变动日志模块要记录日志。
现在问题来了:当股价变化时,这三个模块怎么知道?
轮询检查:低效且不可靠
第一个想法很直接:让每个模块不停地查询股价。
// 交易界面的代码
void TradingUI::run(){
double lastPrice = 0;
while (true) {
double currentPrice = stock_->getPrice();
if (currentPrice != lastPrice) {
updateDisplay(currentPrice);
lastPrice = currentPrice;
}
sleep(1); // 每1毫秒检查一次
}
}
预警模块和日志模块也写了类似的代码,各自不停地轮询。
运行一下,功能是对的,但CPU占用率直线飙升。三个模块同时高频轮询,系统资源被大量浪费。更糟糕的是,如果股价在两次轮询之间快速变化了两次(比如100→105→102),中间那次105的变化就丢失了。
这种方法既浪费资源,又可能丢失关键事件。 显然,我们需要一种更好的方法:让Stock主动通知关心它的模块,而不是让模块来反复询问。
硬编码回调:紧耦合的陷阱
聪明的你很快想到了改进方案:在Stock内部直接调用需要通知的模块。
class Stock {
private:
double price_;
TradingUI* ui_;
AlertSystem* alert_;
Logger* logger_;
public:
void setPrice(double price){
price_ = price;
// 直接通知各个模块
ui_->onPriceChanged(price);
alert_->onPriceChanged(price);
logger_->onPriceChanged(price);
}
};
这下不用轮询了!股价一变,三个模块立刻收到通知。测试一下,CPU占用率降到几乎为零,也不会丢失任何变化。
但一周后,问题来了。产品经理说:“我们要加一个新功能——价格图表模块,也需要监听股价变化。”
你只好打开Stock类,硬着头皮加了一行:
void setPrice(double price){
price_ = price;
ui_->onPriceChanged(price);
alert_->onPriceChanged(price);
logger_->onPriceChanged(price);
chart_->onPriceChanged(price); // 新加的
}
又过了一周,产品经理说:“预警模块下线了,不需要通知它了。” 于是,你又得打开Stock类,删掉对应的一行。
你开始感到不对劲:每次增删一个监听者,都要修改Stock这个核心类的代码。Stock现在“知道”了所有监听者的存在,与它们紧密耦合在一起。
问题暴露了:Stock和监听者之间是硬编码的依赖,无法动态增删,违背了设计模式中推崇的开放-封闭原则。
我们需要一种更优的方法:让Stock不知道具体有谁在监听,但能通知所有人。
观察者模式:使用列表解耦
你灵机一动:用一个列表来存储所有监听者!
首先,定义一个统一的观察者接口:
class IPriceObserver {
public:
virtual void onPriceChanged(double newPrice)= 0;
virtual ~IPriceObserver() = default;
};
然后,让所有具体的监听者类实现这个接口:
class TradingUI : public IPriceObserver {
public:
void onPriceChanged(double newPrice) override{
updateDisplay(newPrice);
}
};
class AlertSystem : public IPriceObserver {
public:
void onPriceChanged(double newPrice) override{
if (newPrice > threshold_) triggerAlert();
}
};
最后,Stock类只维护一个观察者列表,完全不知道列表里具体是谁:
class Stock {
private:
double price_;
std::vector<IPriceObserver*> observers_;
public:
void addObserver(IPriceObserver* observer){
observers_.push_back(observer);
}
void removeObserver(IPriceObserver* observer){
// 从列表中移除
observers_.erase(
std::remove(observers_.begin(), observers_.end(), observer),
observers_.end()
);
}
void setPrice(double price){
price_ = price;
// 通知所有观察者
for (auto* observer : observers_) {
observer->onPriceChanged(price);
}
}
};
现在,可以这样动态地管理监听关系:
Stock stock;
TradingUI ui;
AlertSystem alert;
Logger logger;
stock.addObserver(&ui);
stock.addObserver(&alert);
stock.addObserver(&logger);
stock.setPrice(100.5); // 三个模块同时收到通知!
// 动态下线预警模块
stock.removeObserver(&alert);
stock.setPrice(101.0); // 只有ui和logger收到通知
完美!现在增删监听者完全不需要修改Stock类的核心代码了,实现了松耦合。这正是经典观察者模式的精髓。
但随着系统规模扩大,你又发现了一个新的麻烦。
多对多监听:接口爆炸问题
系统不只有Stock一个被观察者了。现在有三种数据源:
class Stock { ... }; // 股价
class Index { ... }; // 大盘指数
class Exchange { ... }; // 汇率
按照上面的思路,每种数据源都要定义自己的观察者接口:
class IPriceObserver { virtual void onPriceChanged(double)= 0; };
class IIndexObserver { virtual void onIndexChanged(double)= 0; };
class IExchangeObserver { virtual void onExchangeChanged(double)= 0; };
而AlertSystem预警模块需要监听所有这三种数据——它不得不实现三个接口:
class AlertSystem : public IPriceObserver,
public IIndexObserver,
public IExchangeObserver {
void onPriceChanged(double price) override{ ... }
void onIndexChanged(double index) override{ ... }
void onExchangeChanged(double rate) override{ ... }
};
注册监听时,AlertSystem还得主动知道并关联每一个被观察者对象:
stock.addObserver(&alert);
index.addObserver(&alert);
exchange.addObserver(&alert);
新的问题又暴露了:
- 每新增一种数据源,就要新增一个接口,监听者类就要多实现一个接口,导致“接口爆炸”。
- 监听者必须知道所有被观察者的存在,并主动注册,耦合度依然不低。
- 如果
TradingUI也要监听这三种数据,同样的多重继承代码得再写一遍,重复劳动。
有没有办法统一所有的事件,让观察者不需要知道被观察者的具体类型,实现更彻底的解耦?
发布-订阅模式:引入事件总线的终极解耦
一个更巧妙的办法是:引入一个中间人(事件总线),让发布者和订阅者都只和中间人打交道,彼此完全不知情。
class EventBus {
private:
// 事件名 → 回调函数列表
std::map<std::string, std::vector<std::function<void(double)>>> subscribers_;
public:
// 订阅事件
void subscribe(const std::string& event, std::function<void(double)> callback){
subscribers_[event].push_back(callback);
}
// 发布事件
void publish(const std::string& event, double data){
if (subscribers_.find(event) != subscribers_.end()) {
for (auto& callback : subscribers_[event]) {
callback(data);
}
}
}
};
// 全局事件总线
EventBus g_eventBus;
现在,作为发布者的Stock类,完全不知道谁在监听,它只负责发布事件:
class Stock {
private:
double price_;
public:
void setPrice(double price){
price_ = price;
g_eventBus.publish("stock.price.changed", price); // 只管发布
}
};
而作为订阅者的各个模块,也不需要知道Stock的存在,甚至不需要实现任何特定接口。它们只需在初始化时向事件总线订阅感兴趣的事件:
class TradingUI {
public:
TradingUI() {
// 在构造函数里订阅事件
g_eventBus.subscribe("stock.price.changed", [this](double price) {
updateDisplay(price);
});
}
};
class AlertSystem {
public:
AlertSystem() {
g_eventBus.subscribe("stock.price.changed", [this](double price) {
if (price > 100) {
std::cout << "Alert! Price too high!" << std::endl;
}
});
}
};
如果需要监听新的事件,比如大盘指数变化,AlertSystem只需新增一行订阅代码,完全不用修改类结构:
AlertSystem() {
g_eventBus.subscribe("stock.price.changed", [this](double price){...});
// 轻松扩展,监听新事件
g_eventBus.subscribe("index.updated", [this](double index){...});
}
这种架构带来了巨大的灵活性:
- 彻底解耦:发布者和订阅者互不知晓。
- 易于扩展:新增事件类型或订阅者,无需修改现有核心类。
- 支持多对多:一个事件可以被多个模块订阅,一个模块也可以订阅多个事件。
在C++这类系统编程中,理解并灵活运用观察者模式及其变体发布-订阅模式,是构建高内聚、低耦合复杂系统的关键技能。从最原始的轮询,到硬编码回调,再到标准的观察者列表,最后到完全解耦的事件总线,每一次演进都是为了更好地管理模块间的依赖与通信。
如果你在实现这类模式时遇到关于内存管理、线程安全或性能优化等更深层次的问题,欢迎到云栈社区与更多开发者交流探讨,共同解决实际工程中的挑战。