1. 观察者模式实战:用C++和Qt重构气象监测系统
最近在重读《Head First设计模式》,发现书中的观察者模式案例特别适合用Qt来实现。作为一个常年混迹在C++和Qt领域的开发者,我决定用这个经典案例来展示如何在实际项目中应用观察者模式。本文将带你从零开始构建一个完整的气象监测系统,不仅会讲解模式本身,还会分享我在实现过程中踩过的坑和优化技巧。
这个气象站系统需要实时显示温度、湿度和气压数据,并且要支持动态添加和移除显示组件。听起来简单?但当你真正开始编码时,会发现很多设计上的挑战。比如如何避免硬编码的显示更新逻辑?如何确保新加入的显示组件不会影响现有功能?这些问题正是观察者模式要解决的。
2. 系统架构设计
2.1 气象站系统组成
我们的气象监测系统由三个核心组件构成:
- 气象站硬件:负责采集实际的气象数据(温度、湿度、气压)
- WeatherData对象:作为数据中转站,接收硬件数据并管理显示组件
- 显示组件:包括当前状况显示、统计显示和预报显示
cpp复制// WeatherData类的基本结构
class WeatherData {
public:
void measurementsChanged(); // 数据更新时调用
void setTemperature(double temp);
void setHumidity(double humidity);
void setPressure(double pressure);
private:
double temperature;
double humidity;
double pressure;
};
2.2 初始设计的缺陷
很多开发者的第一直觉实现可能是这样的:
cpp复制void WeatherData::measurementsChanged() {
double temp = getTemperature();
double humidity = getHumidity();
double pressure = getPressure();
// 直接调用各个显示组件的更新方法
currentDisplay.update(temp, humidity, pressure);
statsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
}
这种实现至少有三大问题:
- 违反开闭原则:添加新显示组件需要修改WeatherData源码
- 紧耦合:WeatherData需要知道所有显示组件的具体类型
- 缺乏灵活性:无法在运行时动态添加/移除显示组件
3. 观察者模式深度解析
3.1 模式定义与类比
观察者模式定义了对象间的一对多依赖关系,当一个对象(主题)状态改变时,它的所有依赖对象(观察者)都会自动收到通知并更新。这就像杂志订阅:
- 出版社(Subject)维护订阅者(Observer)列表
- 新杂志出版时,自动发送给所有订阅者
- 订阅者可以随时加入或取消订阅
3.2 UML类图解析
观察者模式的核心结构包含两个接口:
-
Subject(主题):
- registerObserver(): 添加观察者
- removeObserver(): 移除观察者
- notifyObservers(): 通知所有观察者
-
Observer(观察者):
- update(): 接收更新通知
mermaid复制classDiagram
class Subject {
+registerObserver()
+removeObserver()
+notifyObservers()
}
class Observer {
+update()
}
class WeatherData {
-temperature
-humidity
-pressure
+measurementsChanged()
}
class Display {
+update()
}
Subject <|-- WeatherData
Observer <|-- Display
WeatherData o-- Observer
3.3 松耦合的优势
观察者模式的最大价值在于它实现了主题和观察者之间的松耦合:
- 主题只知道观察者实现了某个接口,不需要了解具体类
- 可以随时添加新的观察者类型
- 观察者可以独立复用
- 主题或观察者的修改不会相互影响
4. C++实现细节
4.1 主题接口实现
首先定义Observer和Subject接口:
cpp复制// 观察者接口
class Observer {
public:
virtual void update(double temp, double humidity, double pressure) = 0;
virtual ~Observer() = default;
};
// 主题接口
class Subject {
public:
virtual void registerObserver(Observer* o) = 0;
virtual void removeObserver(Observer* o) = 0;
virtual void notifyObservers() = 0;
virtual ~Subject() = default;
};
4.2 WeatherData的具体实现
cpp复制class WeatherData : public Subject {
public:
void registerObserver(Observer* o) override {
observers.insert(o);
}
void removeObserver(Observer* o) override {
observers.erase(o);
}
void notifyObservers() override {
for (auto* o : observers) {
o->update(temperature, humidity, pressure);
}
}
void measurementsChanged() {
notifyObservers();
}
// 设置气象数据的方法
void setMeasurements(double temp, double humidity, double pressure) {
this->temperature = temp;
this->humidity = humidity;
this->pressure = pressure;
measurementsChanged();
}
private:
std::unordered_set<Observer*> observers;
double temperature;
double humidity;
double pressure;
};
4.3 显示组件的实现
以当前状况显示为例:
cpp复制class CurrentConditionsDisplay : public Observer, public QObject {
Q_OBJECT
public:
CurrentConditionsDisplay(Subject* weatherData, QLabel* tempLabel)
: weatherData(weatherData), tempLabel(tempLabel) {
weatherData->registerObserver(this);
}
void update(double temp, double humidity, double pressure) override {
this->temperature = temp;
this->humidity = humidity;
display();
}
void display() {
tempLabel->setText(QString("当前温度: %1°C\n当前湿度: %2%")
.arg(temperature).arg(humidity));
}
private:
Subject* weatherData;
QLabel* tempLabel;
double temperature;
double humidity;
};
5. Qt信号槽与观察者模式
5.1 信号槽的本质
Qt的信号槽机制本身就是观察者模式的典型实现:
- 信号发送者 → Subject
- 信号 → notifyObservers()
- 槽函数 → Observer的update()
cpp复制// 传统connect方式
connect(weatherData, &WeatherData::dataChanged,
currentDisplay, &CurrentDisplay::updateData);
// 对应观察者模式
weatherData->registerObserver(currentDisplay);
5.2 使用Lambda优化
在Qt5中,我们可以用Lambda表达式简化观察者实现:
cpp复制// 在WeatherData中
void notifyObservers() override {
for (auto* o : observers) {
QMetaObject::invokeMethod(o, [this, o]() {
o->update(temperature, humidity, pressure);
});
}
}
// 注册观察者时
weatherData->registerObserver(new Observer([=](double t, double h, double p) {
ui->tempLabel->setText(QString::number(t));
}));
6. 实战中的经验教训
6.1 内存管理陷阱
在C++中实现观察者模式要特别注意对象生命周期:
cpp复制// 错误示例:观察者被销毁后未从主题中移除
{
CurrentConditionsDisplay display(weatherData);
// display离开作用域被销毁
}
// weatherData仍持有已销毁的display指针
// 正确做法:RAII管理
class Display : public Observer {
public:
Display(Subject* s) : subject(s) { s->registerObserver(this); }
~Display() { subject->removeObserver(this); }
private:
Subject* subject;
};
6.2 线程安全考虑
如果主题和观察者可能在不同线程:
- 使用QObject::moveToThread将观察者移到工作线程
- 在notifyObservers()中使用QMetaObject::invokeMethod跨线程调用
- 考虑使用QReadWriteLock保护观察者列表
cpp复制void WeatherData::registerObserver(Observer* o) {
QWriteLocker locker(&lock);
observers.insert(o);
}
void WeatherData::notifyObservers() {
QReadLocker locker(&lock);
for (auto* o : observers) {
QMetaObject::invokeMethod(o, "update", Qt::QueuedConnection,
Q_ARG(double, temperature),
Q_ARG(double, humidity),
Q_ARG(double, pressure));
}
}
7. 模式变体与扩展
7.1 推模型 vs 拉模型
-
推模型:主题将详细数据推送给观察者
cpp复制void update(double temp, double humidity, double pressure); -
拉模型:观察者从主题拉取所需数据
cpp复制void update(Subject* subject) { double temp = subject->getTemperature(); // ... }
7.2 基于事件的扩展
可以扩展为更通用的事件总线系统:
cpp复制class EventBus : public QObject {
Q_OBJECT
public:
static EventBus& instance();
template<typename T>
void publish(const T& event) {
emit eventPublished(QVariant::fromValue(event));
}
template<typename T>
void subscribe(QObject* receiver, void (T::*method)(const typename T::EventType&)) {
connect(this, &EventBus::eventPublished, receiver, [=](const QVariant& event) {
if (event.canConvert<typename T::EventType>()) {
(receiver->*method)(event.value<typename T::EventType>());
}
});
}
signals:
void eventPublished(const QVariant& event);
};
8. 测试与验证
8.1 单元测试示例
使用Qt Test框架测试观察者通知:
cpp复制void TestWeatherStation::testObserverNotification() {
WeatherData weatherData;
MockObserver observer;
weatherData.registerObserver(&observer);
QSignalSpy spy(&observer, &MockObserver::updated);
weatherData.setMeasurements(25.0, 65.0, 1013.0);
QCOMPARE(spy.count(), 1);
QList<QVariant> arguments = spy.takeFirst();
QCOMPARE(arguments.at(0).toDouble(), 25.0);
QCOMPARE(arguments.at(1).toDouble(), 65.0);
QCOMPARE(arguments.at(2).toDouble(), 1013.0);
}
8.2 性能考量
当观察者数量很大时(>1000),需要考虑:
- 使用更高效的数据结构存储观察者(如std::vector)
- 批量通知优化
- 异步通知机制
cpp复制// 批量通知优化示例
void WeatherData::notifyObservers() {
if (observers.empty()) return;
// 准备数据副本避免中途修改
auto data = std::make_shared<WeatherDataSnapshot>(temperature, humidity, pressure);
for (auto* o : observers) {
QThreadPool::globalInstance()->start([o, data]() {
o->update(data->temp, data->humidity, data->pressure);
});
}
}
9. 实际项目中的应用建议
- GUI开发:几乎所有的MVC/MVP架构都使用观察者模式
- 游戏开发:处理用户输入、游戏状态变化等事件
- 分布式系统:节点间状态同步
- 物联网:传感器数据监控
在最近的一个工业监控项目中,我们使用观察者模式实现了:
- 实时数据仪表盘(5个不同视角)
- 异常报警系统(邮件、短信、声光)
- 数据记录模块
- 远程API推送
所有组件都可以在运行时动态配置,核心代码保持简洁:
cpp复制// 核心监控服务
class MonitoringService : public Subject {
public:
void onSensorDataReceived(const SensorData& data) {
this->data = data;
notifyObservers();
}
private:
SensorData data;
};
// 动态添加移除观察者
monitoringService.registerObserver(new DashboardUpdater(ui));
monitoringService.registerObserver(new AlarmTrigger(config));
10. 与其他模式的关系
- 与中介者模式:观察者模式中的Subject类似中介者,但更侧重于单向通知
- 与责任链模式:都可以处理对象间通信,但观察者是广播,责任链是接力
- 与装饰器模式:可以组合使用,创建可动态添加功能的观察者
在复杂系统中,我经常这样组合使用:
cpp复制// 带日志功能的观察者装饰器
class LoggingObserver : public Observer {
public:
LoggingObserver(Observer* wrapped, Logger* logger)
: wrapped(wrapped), logger(logger) {}
void update(double t, double h, double p) override {
logger->log("Before update");
wrapped->update(t, h, p);
logger->log("After update");
}
private:
Observer* wrapped;
Logger* logger;
};
// 使用示例
auto observer = new LoggingObserver(new CurrentConditionsDisplay(), logger);
weatherData.registerObserver(observer);
观察者模式是构建灵活、可扩展系统的利器,特别是在事件驱动架构中。通过这次用C++和Qt的实践,我更加体会到它的强大之处。希望这个案例能帮助你在实际项目中更好地应用这一模式。