【Head First 设计模式】第 2 章:观察者模式 ------ 用现代 C++ 重新解读"交互对象的松耦合
导语 :电台广播不会跑到你家去确认你有没有在听,也不会替你决定你什么时候该关掉收音机。可如果换成 C++ 里的"广播者"(
Subject)持有一堆指向"听众"(Observer)的裸指针,事情就没那么潮洒了------听众突然消失,广播者却毫不知情,继续对着一个已经不存在的对象喊话。本文用气象站案例讲清楚观察者模式,更用std::weak_ptr解决"广播者该不该管听众生死"这个 C++ 特有的问题。
写在前面:从 Java 切换到 C++ 视角后,核心改变点是什么
《Head First 设计模式》用 Java 讲观察者模式时,重点是"一对多的通知关系"和"解耦数据源与展示方式"。这部分思想不受语言影响,换到 C++ 依然成立。
但 Java 版本有一个隐藏假设,很容易被忽略:有垃圾回收器兜底 。观察者忘记调用 removeObserver(),最多是内存泄漏(对象因为还被引用而不会被回收),程序不会立刻崩溃。
C++ 没有这层安全网。如果 Subject 内部存的是指向 Observer 的裸指针或者强引用,而某个 Observer 先于 Subject 销毁却没有注销,notify() 执行到它时就是未定义行为------轻则输出乱码,重则直接崩溃。
反过来,如果 Observer 为了主动"拉"数据也持有 Subject,两者互相持有对方,还会遇到循环引用的问题。
所以这一章要解决两层问题:
- 用观察者模式把"数据变化"和"如何展示"解耦,理解一对多通知关系。
- 用现代 C++ 讲清楚:广播者和听众之间,到底谁该为谁的生命周期负责。
先从一个正在报错边缘的气象站开始。
一、气象站问题:紧耦合的报表系统为什么难扩展
《Head First 设计模式》里的经典场景:气象站测得温度、湿度、气压后,需要同时更新多个报表------当前状况、统计信息、天气预报。
1.1 最直觉的写法:谁都认识谁
刚接触这个需求时,很容易写出这样的代码:
cpp
#include <iostream>
class CurrentConditionsDisplay
{
public:
void update(float temperature, float humidity, float pressure)
{
std::cout << "Current conditions: " << temperature
<< "F degrees and " << humidity << "% humidity\n";
}
};
class StatisticsDisplay
{
public:
void update(float temperature, float humidity, float pressure)
{
std::cout << "Avg/Max/Min stats updated.\n";
}
};
class ForecastDisplay
{
public:
void update(float temperature, float humidity, float pressure)
{
std::cout << "Forecast: more of the same.\n";
}
};
class WeatherData
{
public:
void measurementsChanged()
{
float temperature = getTemperature();
float humidity = getHumidity();
float pressure = getPressure();
currentDisplay_.update(temperature, humidity, pressure);
statisticsDisplay_.update(temperature, humidity, pressure);
forecastDisplay_.update(temperature, humidity, pressure);
}
void setMeasurements(float temperature, float humidity, float pressure)
{
temperature_ = temperature;
humidity_ = humidity;
pressure_ = pressure;
measurementsChanged();
}
private:
float getTemperature() const { return temperature_; }
float getHumidity() const { return humidity_; }
float getPressure() const { return pressure_; }
float temperature_ = 0.0f;
float humidity_ = 0.0f;
float pressure_ = 0.0f;
CurrentConditionsDisplay currentDisplay_;
StatisticsDisplay statisticsDisplay_;
ForecastDisplay forecastDisplay_;
};
这段代码能跑,输出也符合预期。问题不在语法,而在依赖关系:WeatherData 的头文件里,必须 #include 每一种具体 Display 的头文件。
1.2 暴露的问题
假设现在要新增一个第三方报表 ThirdPartyDisplay,步骤会是:
- 打开
WeatherData的源码。 - 在成员变量里新增一个
thirdPartyDisplay_。 - 在
measurementsChanged()里新增一行thirdPartyDisplay_.update(...)。 - 重新编译
WeatherData------即便它的核心逻辑(测量数据)根本没有变化。
WeatherData 本该只负责"管理气象数据",现在却被迫认识每一个具体的报表类型,还要在数据变化时手动挨个通知。新增一种报表,代价是修改一个本不该被修改的类。
1.3 定位真正变化的部分
和第1章鸭子案例的分析方法一样,先问一句:这里什么是稳定的,什么是易变的?
- 相对稳定的:气象站测量数据的逻辑(
getTemperature()等)、数据变化后"需要通知关心的对象"这件事本身。 - 容易变化的:谁在关心数据变化------报表的种类会增加、减少,甚至运行时动态开关。
WeatherData 不应该硬编码"通知哪几个具体对象",而应该只负责"数据变了,通知所有关心的人",不关心"关心的人"具体是谁。这正是观察者模式要解决的问题。
二、先建立直觉:广播 vs 主动去问
在看代码前,先把"被观察者"和"观察者"这两个角色说清楚。
2.1 被观察者(Subject):像电台广播
电台主持人开播时,不知道也不关心具体是谁在收听。他只管把信号发出去,收音机开着的人自然会听到。这就是 Subject 的角色:
我的状态变化了,我会发出通知。谁在听,我不关心;听众自己决定什么时候订阅、什么时候退订。
WeatherData 就应该是这样的广播者:它不认识 CurrentConditionsDisplay,只认识"观察者"这个抽象身份。
2.2 观察者(Observer):像订阅了电台的听众
反过来,Observer 的角色是主动订阅:
我对你的状态变化感兴趣,请把我加入你的通知列表。我随时可以退订,你不该替我决定我什么时候不想再听了。
这里有个容易被忽略但很关键的比喻延伸:广播站不会因为一个听众家里断电(对象销毁)就自动清空他的订阅记录 ------现实中的电台无所谓,但 C++ 里的 Subject 如果不知道听众已经"断电",继续对着空气喊话,就是程序层面的未定义行为。这也是本章第四节要处理的核心问题。
2.3 推模型 vs 拉模型
通知的方式有两种:
- 推模型(Push) :
Subject在notify()时,直接把最新的温度、湿度、气压当作参数传给每个Observer::update(...)。 - 拉模型(Pull) :
Subject只发一个"数据变了"的信号,Observer收到信号后自己反过来调用Subject的 getter,主动去"拉"最新状态。
气象站案例用推模型更自然,因为每次通知都需要三个数据,参数不多不少刚好合适。拉模型更适合观察者只需要其中一部分数据、或者数据种类经常变化的场景。本章以推模型为主线,第四节会提到拉模型带来的所有权问题。
2.4 解耦的本质
把这两个角色的关系画出来:
text
重构前:WeatherData 直接认识每一种具体 Display
重构后:WeatherData 只认识 Observer 接口
CurrentConditionsDisplay / StatisticsDisplay / ForecastDisplay
都只是"实现了 Observer 接口的某个类"
WeatherData 的代码里,从此不会出现任何一个具体 Display 类的名字。这正是"面向接口编程,而不是面向实现编程"在观察者模式里的体现。
三、观察者模式:定义一对多的通知关系
先用一句朴素的话理解观察者模式:
定义对象之间的一对多依赖关系,当一个对象的状态改变时,所有依赖它的对象都会自动收到通知并更新。
3.1 定义 Observer 与 Subject 接口
cpp
// 观察者接口
class Observer
{
public:
virtual ~Observer() = default;
virtual void update(float temperature, float humidity, float pressure) = 0;
};
// 被观察者(Subject)接口
class Subject
{
public:
virtual ~Subject() = default;
virtual void registerObserver(std::weak_ptr<Observer> observer) = 0;
virtual void removeObserver(const std::shared_ptr<Observer>& observer) = 0;
virtual void notifyObservers() = 0;
};
和第1章一样,两个接口都需要虚析构函数。这里先直接用 std::weak_ptr<Observer> 作为 registerObserver 的参数类型------原因会在第四节详细展开,本节先把基本结构搭起来。
3.2 WeatherData 实现 Subject
cpp
#include <algorithm>
#include <memory>
#include <vector>
class WeatherData final : public Subject
{
public:
void registerObserver(std::weak_ptr<Observer> observer) override
{
observers_.push_back(std::move(observer));
}
// 移除指定的观察者,同时自动清理已经失效的弱引用
void removeObserver(const std::shared_ptr<Observer>& observer) override
{
// 采用 C++ 标准库经典的"Erase-remove 范式"(Erase-remove idiom)来高效删除元素。
// std::remove_if 负责把"需要保留的元素"移到 vector 前面,返回一个分隔两区的迭代器;
// observers_.erase 负责把后面那段"不需要的元素"在物理内存中真正抹去。
observers_.erase(
std::remove_if(observers_.begin(), observers_.end(),
// Lambda 表达式(匿名闭包函数):作为谓词传递给 remove_if。
// [&observer] 表示按引用捕获外部传入的待删除指针 observer,避免指针拷贝。
[&observer](const std::weak_ptr<Observer>& weak)
{
// 1. 尝试将 weak_ptr 提升为强引用 shared_ptr
// 因为 weak_ptr 本身无法直接获取指向的对象或进行比较,必须调用 lock()。
// 如果原对象还活着,locked 会是一个指向该对象的 shared_ptr;
// 如果原对象已经被析构(在外部被销毁),locked 会是一个空的 shared_ptr (nullptr)。
auto locked = weak.lock();
// 2. 返回布尔值告诉 remove_if 这个元素要不要被删掉 (true = 删除, false = 保留):
// 条件一:!locked
// 说明对象在外部已经被销毁了(悬空引用)。既然对象都没了,
// 留在 observers_ 列表里也是浪费空间,顺便把它清理掉(懒清理机制)。
// 条件二:locked == observer
// 说明提升后的 shared_ptr 和我们要寻找并注销的观察者地址相同(指向同一个内存对象)。
// 只要满足其中任意一个条件(用 || 连接),该元素就会被remove_if标记为删除。
return !locked || locked == observer;
}),
observers_.end());
}
void notifyObservers() override
{
for (auto& weak : observers_)
{
// C++ 条件初始化语法糖:
// 1. 调用 weak.lock() 尝试将弱引用提升为 shared_ptr
// 2. 在 if 内部定义临时变量 observer
// 3. 如果对象还没被销毁,observer 非空(条件为 true),进入分支
// 4. observer 强引用会在 if 作用域内保持对象存活,防止执行 update 时对象突遭销毁
if (auto observer = weak.lock())
{
observer->update(temperature_, humidity_, pressure_);
}
}
}
void setMeasurements(float temperature, float humidity, float pressure)
{
temperature_ = temperature;
humidity_ = humidity;
pressure_ = pressure;
measurementsChanged();
}
private:
void measurementsChanged()
{
notifyObservers();
}
float temperature_ = 0.0f;
float humidity_ = 0.0f;
float pressure_ = 0.0f;
std::vector<std::weak_ptr<Observer>> observers_;
};
留意这几处设计:
observers_存的是std::weak_ptr<Observer>,不是强引用------WeatherData不应该决定观察者的生死,这一点会在第四节详细展开。notifyObservers()里用weak.lock()判断观察者是否还活着,活着才通知,已经销毁的直接跳过。removeObserver()顺手清理掉已经失效(lock()返回空)的条目,避免列表越攒越大。measurementsChanged()保留为私有方法,对外只暴露setMeasurements()------这和书中原始设计一致,调用方不需要关心"数据变化"和"通知"是分两步走的。
3.3 具体 Display 实现 Observer
cpp
#include <iostream>
//std::enable_shared_from_this<T> 是 C++11 引入的一个模板类,主要用来解决一个关键问题:在一个被 std::shared_ptr 管理的对象内部,安全地获取指向自身的 std::shared_ptr
class CurrentConditionsDisplay final : public Observer,
public std::enable_shared_from_this<CurrentConditionsDisplay>
{
public:
void update(float temperature, float humidity, float pressure) override
{
std::cout << "Current conditions: " << temperature
<< "F degrees and " << humidity << "% humidity\n";
}
};
class StatisticsDisplay final : public Observer,
public std::enable_shared_from_this<StatisticsDisplay>
{
public:
void update(float temperature, float humidity, float pressure) override
{
maxTemp_ = std::max(maxTemp_, temperature);
minTemp_ = std::min(minTemp_, temperature);
sumTemp_ += temperature;
++count_;
std::cout << "Avg/Max/Min temperature: "
<< sumTemp_ / count_ << "/" << maxTemp_ << "/" << minTemp_ << '\n';
}
private:
float maxTemp_ = std::numeric_limits<float>::lowest();
float minTemp_ = std::numeric_limits<float>::max();
float sumTemp_ = 0.0f;
int count_ = 0;
};
class ForecastDisplay final : public Observer,
public std::enable_shared_from_this<ForecastDisplay>
{
public:
void update(float temperature, float humidity, float pressure) override
{
std::cout << "Forecast: more of the same.\n";
}
};
三个 Display 都继承了 std::enable_shared_from_this。原因很直接:如果这个类的实例将来要把自己注册成观察者(registerObserver(shared_from_this())),就需要一种安全的方式从"我自己"生成一个 std::weak_ptr------直接构造 std::shared_ptr<CurrentConditionsDisplay>(this) 会导致引用计数被重复管理,最终重复释放。这一点第四节会展开讲。
3.4 组装并运行
cpp
int main()
{
WeatherData weatherData;
auto currentDisplay = std::make_shared<CurrentConditionsDisplay>();
auto statisticsDisplay = std::make_shared<StatisticsDisplay>();
auto forecastDisplay = std::make_shared<ForecastDisplay>();
weatherData.registerObserver(currentDisplay);
weatherData.registerObserver(statisticsDisplay);
weatherData.registerObserver(forecastDisplay);
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
weatherData.removeObserver(forecastDisplay);
weatherData.setMeasurements(78, 90, 29.2f);
}
输出:
text
Current conditions: 80F degrees and 65% humidity
Avg/Max/Min temperature: 80/80/80
Forecast: more of the same.
Current conditions: 82F degrees and 70% humidity
Avg/Max/Min temperature: 81/82/80
Forecast: more of the same.
Current conditions: 78F degrees and 90% humidity
Avg/Max/Min temperature: 80/82/78
第三次 setMeasurements() 之后,ForecastDisplay 已经被 removeObserver() 移除,所以它不再收到通知,输出里也就没有再出现"Forecast: more of the same."。
registerObserver 接收 std::weak_ptr<Observer>,但传进去的是 std::shared_ptr------这是合法的隐式转换,shared_ptr 可以直接转成对应的 weak_ptr。weatherData 本身不持有任何一个 Display 的强引用,三个 shared_ptr 的生命周期完全由 main() 的局部变量控制。
四、C++ 核心解构:悬空指针与生命周期地雷
上一节的代码能正确运行,但那是因为三个 shared_ptr 一直活到 main() 结束。如果情况反过来呢?
4.1 最大风险场景:Observer 先于 Subject 销毁
假设 WeatherData 内部存的不是 weak_ptr,而是裸指针:
cpp
std::vector<Observer*> observers_; // 危险写法,仅用于说明问题
再看这样一段调用:
cpp
void run(WeatherData& weatherData)
{
CurrentConditionsDisplay display;
weatherData.registerObserver(&display);
} // display 在这里被销毁
int main()
{
WeatherData weatherData;
run(weatherData);
weatherData.setMeasurements(80, 65, 30.4f); // 未定义行为
}
display 是 run() 里的局部对象,函数返回时就销毁了。但 weatherData.observers_ 里仍然存着指向它的裸指针。等到 setMeasurements() 触发 notifyObservers(),代码会对着一块已经释放的内存调用虚函数------这就是悬空指针(dangling pointer),结果可能是崩溃,也可能是更隐蔽的数据错乱。
这正是本章开头提到的"Java 的隐藏假设":Java 里 display 只要还被 observers 列表引用,就不会被回收;C++ 里对象的生命周期由作用域决定,和"是否还被引用"完全无关。
4.2 用 std::weak_ptr 表达"广播者不该决定听众的生死"
第三节的代码已经给出了答案:WeatherData 内部存的是 std::vector<std::weak_ptr<Observer>>,不是裸指针,也不是 std::shared_ptr。这个选择背后的逻辑是:
观察者的生命周期应该由持有它的那一方(通常是
main()或者更上层的对象)决定,WeatherData只是"知道"这些观察者的存在,不应该、也不需要延长它们的生命周期。
如果把 registerObserver 的参数类型换成 std::shared_ptr<Observer> 并直接存起来,WeatherData 就会成为观察者的共同持有者之一。这看起来"更安全",实际上引入了一个新问题:观察者的生命周期会被 WeatherData 不知不觉地延长,即使调用方已经不再持有它、以为它该销毁了,它依然活着,因为 WeatherData 还在。
weak_ptr 不参与引用计数,不会影响对象的生死,只负责"在需要的时候确认它还活着"。这正是 4.1 节问题的正确解法:
cpp
void notifyObservers() override
{
for (auto& weak : observers_)
{
if (auto observer = weak.lock()) // 判活
{
observer->update(temperature_, humidity_, pressure_);
}
// lock() 返回空,说明观察者已经销毁,直接跳过,不会崩溃
}
}
回到 4.1 节的例子,如果 observers_ 存的是 weak_ptr,display 销毁后,对应的 weak_ptr::lock() 会返回空指针,notifyObservers() 只是安静地跳过它,不会有任何未定义行为。
4.3 循环引用陷阱:拉模型下的反向持有
2.3 节提到过拉模型------观察者收到通知后,反过来调用 Subject 的接口去"拉"数据。这要求 Observer 也持有一份指向 Subject 的引用:
cpp
class PullBasedDisplay final : public Observer
{
public:
explicit PullBasedDisplay(std::shared_ptr<WeatherData> subject)
: subject_(std::move(subject)) // 危险:双向 shared_ptr
{
}
void update(float, float, float) override
{
std::cout << subject_->getTemperature() << '\n';
}
private:
std::shared_ptr<WeatherData> subject_;
};
如果 WeatherData 那边又用 shared_ptr 强引用这个 PullBasedDisplay,就形成了经典的循环引用:WeatherData 持有 Display,Display 持有 WeatherData,两者的引用计数永远不会归零,谁都不会被析构,内存泄漏且不会有任何报错提示你。
解法很直接:两个方向里,至少有一个必须是弱引用 。观察者模式里几乎总是"Subject 弱引用 Observer"这个方向更合理(呼应 4.2 节的结论),如果 Observer 也要反向持有 Subject,应该用裸指针或 std::weak_ptr<WeatherData>,而不是 shared_ptr:
cpp
private:
std::weak_ptr<WeatherData> subject_; // 安全:不参与引用计数
4.4 RAII 自动注销:不必依赖手动调用 removeObserver
第三节的 main() 里手动调用了一次 removeObserver(forecastDisplay)。如果忘记调这一行会怎样?由于 observers_ 存的是 weak_ptr,forecastDisplay 销毁后 notifyObservers() 依然会安全跳过它,不会崩溃------但列表里会一直留着一个"已失效"的条目,直到下一次 removeObserver() 顺手清理,或者本节末尾的技巧主动清理。
如果希望观察者析构时自动完成注销,可以引入一层轻量的包装,在析构函数里调用 removeObserver:
cpp
class ScopedObserver
{
public:
ScopedObserver(std::shared_ptr<Subject> subject,
std::shared_ptr<Observer> observer)
: subject_(std::move(subject)), observer_(std::move(observer))
{
subject_->registerObserver(observer_);
}
~ScopedObserver()
{
subject_->removeObserver(observer_);
}
private:
std::shared_ptr<Subject> subject_;
std::shared_ptr<Observer> observer_;
};
这是 RAII(Resource Acquisition Is Initialization)思想在观察者模式里的应用:把"注册"和"注销"这一对操作绑定到一个对象的构造和析构上,调用方不需要记得手动配对调用。对于生命周期明确、订阅关系和某个作用域强绑定的场景,这比裸调用 registerObserver/removeObserver 更不容易出错。
4.5 小结:三条生命周期规则
把本节的结论收拢成三条规则:
Subject 存观察者时,优先用weak_ptr,不要用裸指针(会悬空),也不要不假思索用shared_ptr`(会不必要地延长生命周期)。- 拉模型下
Observer反向持有Subject时,同样避免双向shared_ptr,至少一个方向要弱引用。 - 如果订阅关系和某个作用域强绑定,用 RAII 包装注册/注销这一对操作,而不是依赖"记得手动调用"。
五、Modern C++ 简化版:从接口类到 Signal/Slot
三个 Display 目前都只做一件事:接收三个 float,打印一行文字。为了这么简单的逻辑,专门定义一个 Observer 接口、再各写一个实现类,未免有点"仪式感过重"。C++11 之后,std::function 和 Lambda 给了另一种落地方式。
同样先澄清:这不是 Java 做不到的事,现代 Java 也有函数式接口。这里讨论的是 C++ 里可调用对象带来的另一种写法。
5.1 手写一个极简 Signal
cpp
#include <functional>
#include <vector>
template <typename... Args>
class Signal
{
public:
using Slot = std::function<void(Args...)>;
void connect(Slot slot)
{
slots_.push_back(std::move(slot));
}
void emit(Args... args) const
{
for (const auto& slot : slots_)
{
slot(args...);
}
}
private:
std::vector<Slot> slots_;
};
Signal<float, float, float> 就相当于第三节里 Subject 接口 + notifyObservers() 的合体:connect() 对应 registerObserver(),emit() 对应 notifyObservers()。
5.2 用 Signal 重写 WeatherData
cpp
class FunctionalWeatherData
{
public:
Signal<float, float, float> measurementsChanged;
void setMeasurements(float temperature, float humidity, float pressure)
{
temperature_ = temperature;
humidity_ = humidity;
pressure_ = pressure;
measurementsChanged.emit(temperature_, humidity_, pressure_);
}
private:
float temperature_ = 0.0f;
float humidity_ = 0.0f;
float pressure_ = 0.0f;
};
订阅时直接用 Lambda,不需要专门定义 CurrentConditionsDisplay 类:
cpp
int main()
{
FunctionalWeatherData weatherData;
weatherData.measurementsChanged.connect(
[](float temperature, float humidity, float pressure)
{
std::cout << "Current conditions: " << temperature
<< "F degrees and " << humidity << "% humidity\n";
});
float maxTemp = std::numeric_limits<float>::lowest();
weatherData.measurementsChanged.connect(
[maxTemp](float temperature, float, float) mutable
{
maxTemp = std::max(maxTemp, temperature);
std::cout << "Max temperature so far: " << maxTemp << '\n';
});
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
}
没有 Observer 接口,没有具体 Display 类,connect() 接收的就是一段"数据变化时该做什么"的逻辑。
5.3 接口类版 vs Signal/Lambda 版怎么选
| 场景 | 更适合接口类版 | 更适合 Signal/Lambda 版 |
|---|---|---|
| 观察者逻辑只有几行 | ✓ | |
| 观察者需要维护复杂内部状态(如 StatisticsDisplay 的累计值) | ✓ | |
| 需要在观察者析构时自动注销(配合 4.4 节 RAII) | ✓ | |
| 临时订阅一次性逻辑,或写测试时快速插入断言 | ✓ | |
| 希望观察者有清晰的类型名称、可被其他模块引用 | ✓ |
StatisticsDisplay 这种带累计状态、名字本身就说明意图的观察者,写成接口类实现更清楚;main() 里临时订阅一行打印逻辑,用 Lambda 更轻便。
5.4 Lambda 版本依然要面对生命周期问题
Signal 换掉了接口类,但没有换掉第四节讨论的所有权问题。如果 Lambda 按引用捕获了一个即将销毁的对象(或者捕获了 this,而持有该 Lambda 的对象析构得比 Signal 早),一样会留下悬空引用------这一点第1章 5.3 节已经详细讨论过,此处不再重复展开,结论同样适用:优先按值捕获,捕获 this 时确认生命周期关系。
六、学以致用:从气象站迁移到传感器订阅
设计模式真正的价值,不是记住气象站,而是能认出其他问题里的相同结构。
现在有一个场景:一块温度传感器,多个订阅者关心它的读数------日志记录器负责落盘,超温告警器负责报警,显示屏负责实时展示。
映射关系很直接:
| 气象站系统 | 传感器系统 |
|---|---|
WeatherData |
TemperatureSensor |
Observer |
Observer(沿用同一个接口) |
CurrentConditionsDisplay |
SensorLogger |
StatisticsDisplay |
OverheatAlarm |
6.1 复用 Observer 接口,新增两个订阅者
传感器场景只关心一个数值,接口比气象站还简单:
cpp
class Observer
{
public:
virtual ~Observer() = default;
virtual void update(float value) = 0;
};
class TemperatureSensor final : public Subject
{
public:
void registerObserver(std::weak_ptr<Observer> observer) override
{
observers_.push_back(std::move(observer));
}
void removeObserver(const std::shared_ptr<Observer>& observer) override
{
observers_.erase(
std::remove_if(observers_.begin(), observers_.end(),
[&observer](const std::weak_ptr<Observer>& weak)
{
auto locked = weak.lock();
return !locked || locked == observer;
}),
observers_.end());
}
void notifyObservers() override
{
for (auto& weak : observers_)
{
if (auto observer = weak.lock())
{
observer->update(reading_);
}
}
}
void setReading(float reading)
{
reading_ = reading;
notifyObservers();
}
private:
float reading_ = 0.0f;
std::vector<std::weak_ptr<Observer>> observers_;
};
cpp
class SensorLogger final : public Observer
{
public:
void update(float value) override
{
std::cout << "[log] reading = " << value << '\n';
}
};
class OverheatAlarm final : public Observer
{
public:
void update(float value) override
{
if (value > threshold_)
{
std::cout << "[alarm] overheat! reading = " << value << '\n';
}
}
private:
float threshold_ = 75.0f;
};
cpp
int main()
{
TemperatureSensor sensor;
auto logger = std::make_shared<SensorLogger>();
auto alarm = std::make_shared<OverheatAlarm>();
sensor.registerObserver(logger);
sensor.registerObserver(alarm);
sensor.setReading(60.0f);
sensor.setReading(80.0f);
}
输出:
text
[log] reading = 60
[log] reading = 80
[alarm] overheat! reading = 80
结构和气象站案例完全一致:TemperatureSensor 不认识 SensorLogger 或 OverheatAlarm 的具体类型,只认识 Observer 接口;observers_ 依然是 std::weak_ptr,不会替订阅者的生死做决定。新增一种订阅者(比如把读数上报到云端),只需要新写一个 Observer 实现,不用改动 TemperatureSensor。
6.2 运行时增删订阅者:显示屏只在"值班"时监听
工程场景里,订阅关系往往不是固定不变的。比如一块巡检显示屏,只在巡检员打开它的这段时间需要关注读数,巡检结束就该退订:
cpp
class InspectionDisplay final : public Observer
{
public:
void update(float value) override
{
std::cout << "[display] current reading = " << value << '\n';
}
};
void runInspection(TemperatureSensor& sensor)
{
auto display = std::make_shared<InspectionDisplay>();
sensor.registerObserver(display);
sensor.setReading(65.0f); // 巡检期间正常收到通知
sensor.removeObserver(display);
} // display 在这里销毁,但已经提前退订,不会留下悬空的 weak_ptr
int main()
{
TemperatureSensor sensor;
runInspection(sensor);
sensor.setReading(90.0f); // 巡检已结束,没有任何输出
}
runInspection() 返回后 display 立刻销毁,但因为提前调用了 removeObserver(),observers_ 里不会留下失效条目------这是本章第四节"RAII 自动注销"思路的一个简化版本:订阅的生命周期和"巡检"这个作用域强绑定,进入巡检就注册,巡检结束就注销。
6.3 复现一次生命周期陷阱:忘记退订会怎样
刻意去掉 removeObserver() 这一行,看看第四节讨论的问题在这个场景里如何体现:
cpp
void runInspectionWithoutCleanup(TemperatureSensor& sensor)
{
auto display = std::make_shared<InspectionDisplay>();
sensor.registerObserver(display);
sensor.setReading(65.0f);
} // 忘记调用 removeObserver,display 在这里销毁
int main()
{
TemperatureSensor sensor;
runInspectionWithoutCleanup(sensor);
sensor.setReading(90.0f); // display 已经不存在
}
因为 observers_ 存的是 std::weak_ptr,不是裸指针,这里不会崩溃------weak.lock() 会返回空指针,notifyObservers() 安静地跳过它。这正是 4.2 节的结论在传感器场景里的直接验证:同样是"忘记清理"的疏忽,裸指针版本会导致未定义行为,weak_ptr 版本只是留下一条不会被触发的失效记录。
如果这个失效记录需要被及时清理(比如订阅者数量很大、长期运行的服务),可以参考 3.2 节 removeObserver() 里 std::remove_if 的写法,定期或在下一次操作时清扫一遍列表。
七、回头看:观察者模式解决了什么
观察者模式不是为了炫技式地引入接口和智能指针,它主要解决的是:一个对象的状态变化,需要让若干个数量不固定、类型可能各不相同的对象知道,而这些对象又不该和状态源死死绑在一起。
重构前,WeatherData 的代码里直接写死了三个具体 Display:
text
WeatherData
├── 认识 CurrentConditionsDisplay
├── 认识 StatisticsDisplay
└── 认识 ForecastDisplay
重构后,WeatherData 只认识一个抽象接口:
text
WeatherData ──依赖──> Observer 接口
├── CurrentConditionsDisplay
├── StatisticsDisplay
└── ForecastDisplay(可随时增删)
新增一种报表,不需要改动 WeatherData;某个报表不再需要,也不需要改动 WeatherData。这两个方向的变化被解耦了。
7.1 这也体现了开闭原则
新增 ThirdPartyDisplay 时,只需要新写一个实现 Observer 的类,不需要修改 WeatherData::notifyObservers()。这正是:
对扩展开放,对修改关闭。
策略模式让"一个对象怎么做某件事"可以独立替换;观察者模式让"一件事发生后该通知谁"可以独立增删。两者都是开闭原则在不同维度上的体现。
7.2 什么时候不必用观察者模式
如果订阅关系是固定的、订阅者数量从一开始就不会变、也没有运行时增删的需求,直接在数据变化处调用几个具体函数,往往比引入接口、weak_ptr 和通知列表更直接。
可以在出现以下信号时考虑观察者模式:
- 一个对象状态变化后,需要通知的对象数量不固定,或者会随时间增减。
- 状态源不应该知道、也不应该依赖具体是哪些对象在关心它的变化。
- 希望在不修改状态源代码的前提下,新增或移除某类响应逻辑。
- 需要在测试中注入一个"假的"观察者来验证状态变化是否被正确广播。
八、本章 C++ 避坑卡片
卡片 1:广播者存观察者时,优先 weak_ptr,而不是 shared_ptr 或裸指针
cpp
std::vector<std::weak_ptr<Observer>> observers_;
裸指针会在观察者先销毁时变成悬空指针,导致未定义行为;shared_ptr 会让广播者无意中延长观察者的生命周期。weak_ptr 准确表达了"广播者不该决定听众的生死"。
卡片 2:通知前用 lock() 判活,不要假设列表里的对象一定还存在
cpp
if (auto observer = weak.lock())
{
observer->update(...);
}
weak_ptr 本身不能直接调用接口方法,必须先 lock() 成 shared_ptr 确认对象还活着。忘记判活、直接对 weak_ptr 解引用是编译错误,但如果退化成裸指针存储,同样的疏忽会变成运行时的未定义行为。
卡片 3:双向持有时,至少一个方向要是弱引用
拉模型下 Observer 反向持有 Subject 很常见。如果 Subject 强引用 Observer、Observer 又强引用 Subject,两者会陷入循环引用,谁都不会被析构。规则很简单:一对相互引用的对象,至少一个方向必须是 weak_ptr 或裸指针。
卡片 4:通知过程中增删观察者,要小心迭代器失效
如果某个观察者在自己的 update() 里调用 removeObserver() 注销自己,或者新增了一个观察者,而 notifyObservers() 正在遍历同一个 observers_,就有迭代器失效的风险。稳妥的做法是通知前先拷贝一份快照再遍历,或者用索引而不是迭代器遍历。
九、本章小结
观察者模式最值得带走的,不是 Observer 这个接口名,而是一种拆分"状态"与"响应状态"的思路:
- 找出一个对象状态变化后,可能需要通知的、数量不固定的响应者。
- 为这些响应者定义统一的通知接口,让广播者不认识任何具体实现。
- 广播者只负责"发出通知",不负责决定响应者的生死------这一点在 C++ 里必须靠
weak_ptr明确表达出来,Java 的 GC 不会替你兜底。 - 双向持有时,永远留一个方向的弱引用,避免循环引用。
- 简单的响应逻辑可以用
std::function和 Lambda(Signal/Slot),复杂状态、需要自动生命周期管理的响应者,保留接口类实现。
回到本章标题的那句话:广播者该不该管听众的生死?答案是不该------但"不该管"不等于"不用管",而是要用 weak_ptr 把这层关系写进类型系统,让编译器和运行时都清楚谁对谁的生命周期负责。
附:编译版本说明
std::function、Lambda、std::weak_ptr、std::enable_shared_from_this:C++11。std::make_shared:C++11。- 本文完整示例建议使用:
bash
g++ -std=c++14 -Wall -Wextra -pedantic observer.cpp -o observer
如果项目使用 C++17、C++20 或更高标准,文中的写法同样适用。