在软件系统的设计中,我们经常会遇到「对象间联动」的场景:订单状态更新后,需要同时通知库存系统、物流系统、用户消息中心;配置参数修改后,所有依赖该配置的模块都要重新加载。如果让每个模块主动轮询状态,不仅效率低下,还会让代码高度耦合。
观察者模式正是为解决这类「一对多依赖、状态联动通知」问题而生的经典设计模式。它在保证对象间低耦合的前提下,实现了状态变化的自动广播与响应,是行为型设计模式中应用最广泛的模式之一。
一、模式核心定义
观察者模式(Observer Pattern)也被称为发布 - 订阅模式、模型 - 视图模式,其正式定义为:
定义对象间的一对多依赖关系,使得每当一个对象(主题 / 被观察者)的状态发生改变时,所有依赖于它的对象(观察者)都能自动收到通知并同步更新。
它的核心设计思想是解耦发布者与订阅者:二者只依赖抽象接口,不依赖对方的具体实现,各自可以独立变化与扩展。
二、核心角色与类结构
观察者模式包含四个分工明确的核心角色,共同构成完整的通知链路:
1. 抽象主题(Subject / Observable)
被观察者的统一抽象接口,声明三个核心能力:
attach():注册、添加新的观察者detach():注销、移除已有的观察者notify():状态变化时,遍历并通知所有已注册的观察者
2. 具体主题(Concrete Subject)
抽象主题的实现类,内部维护一份观察者列表,同时持有自身的业务状态。当自身状态发生变更时,主动调用notify()方法向所有观察者广播状态变化。
3. 抽象观察者(Observer)
所有观察者的统一接口,仅声明一个update()更新方法,作为收到通知后的响应入口。
4. 具体观察者(Concrete Observer)
抽象观察者的实现类,实现update()方法,在收到主题的通知后,执行自身的业务逻辑(如刷新界面、记录日志、触发操作等)。
整体结构关系可以用如下逻辑表示:
bash
ConcreteSubject 持有 → Observer 列表
↑ 继承 ↑ 继承
Subject(抽象接口) Observer(抽象接口)
↓ 通知 ↓ 实现
ConcreteObserver 响应 → 状态更新
三、C++ 代码实现
我们通过两个版本逐步实现:先通过基础裸指针版本还原模式本质,再结合智能指针实现工业级安全版本,解决生命周期与循环引用问题。
版本 1:基础实现(裸指针版)
最直观地体现观察者模式的核心结构,适合理解原理。
cpp
#include <iostream>
#include <vector>
#include <string>
// 抽象观察者:定义更新接口
class Observer {
public:
virtual ~Observer() = default;
virtual void update(const std::string& state) = 0;
};
// 抽象主题:定义注册、移除、通知接口
class Subject {
public:
virtual ~Subject() = default;
virtual void attach(Observer* observer) = 0;
virtual void detach(Observer* observer) = 0;
virtual void notify() = 0;
};
// 具体主题:消息发布器
class MessagePublisher : public Subject {
private:
std::vector<Observer*> observers; // 观察者列表
std::string message; // 主题状态:最新消息
public:
void attach(Observer* observer) override {
observers.push_back(observer);
}
void detach(Observer* observer) override {
for (auto it = observers.begin(); it != observers.end(); ++it) {
if (*it == observer) {
observers.erase(it);
break;
}
}
}
void notify() override {
for (Observer* obs : observers) {
obs->update(message);
}
}
// 发布新消息,触发状态变化与通知
void publishMessage(const std::string& msg) {
message = msg;
std::cout << "【发布器】发布新消息:" << msg << std::endl;
notify();
}
};
// 具体观察者1:用户终端
class UserTerminal : public Observer {
private:
std::string name;
public:
explicit UserTerminal(std::string n) : name(std::move(n)) {}
void update(const std::string& state) override {
std::cout << "【终端" << name << "】收到消息:" << state << std::endl;
}
};
// 具体观察者2:日志系统
class LogSystem : public Observer {
public:
void update(const std::string& state) override {
std::cout << "【日志系统】记录消息:" << state << std::endl;
}
};
// 客户端代码
int main() {
MessagePublisher publisher;
UserTerminal user1("小明");
UserTerminal user2("小红");
LogSystem log;
// 注册观察者
publisher.attach(&user1);
publisher.attach(&user2);
publisher.attach(&log);
// 发布第一条消息,自动通知所有观察者
publisher.publishMessage("系统将于今晚22点维护");
std::cout << "\n--- 移除用户小红后 ---" << std::endl;
publisher.detach(&user2);
publisher.publishMessage("维护时间调整为23点");
return 0;
}
运行结果:
bash
【发布器】发布新消息:系统将于今晚22点维护
【终端小明】收到消息:系统将于今晚22点维护
【终端小红】收到消息:系统将于今晚22点维护
【日志系统】记录消息:系统将于今晚22点维护
--- 移除用户小红后 ---
【发布器】发布新消息:维护时间调整为23点
【终端小明】收到消息:维护时间调整为23点
【日志系统】记录消息:维护时间调整为23点
版本 2:智能指针安全实现(解决循环引用)
在 C++ 工程开发中,裸指针版本存在两个致命问题:
- 观察者提前销毁后,主题会访问野指针,引发程序崩溃
- 如果观察者同时持有主题的
shared_ptr,会形成循环引用导致内存泄漏
工业级的解决方案是:主题用weak_ptr<Observer>存储观察者,不增加强引用计数,完美打破循环,同时可以自动感知对象生命周期。
cpp
#include <iostream>
#include <vector>
#include <string>
#include <memory>
// 前置声明
class Observer;
// 抽象主题
class Subject {
public:
virtual ~Subject() = default;
virtual void attach(std::shared_ptr<Observer> observer) = 0;
virtual void detach(std::shared_ptr<Observer> observer) = 0;
virtual void notify() = 0;
};
// 抽象观察者
class Observer : public std::enable_shared_from_this<Observer> {
public:
virtual ~Observer() = default;
virtual void update(const std::string& state) = 0;
};
// 具体主题:安全发布器
class SafePublisher : public Subject, public std::enable_shared_from_this<SafePublisher> {
private:
std::vector<std::weak_ptr<Observer>> observers; // weak_ptr存储,避免循环引用
std::string message;
public:
void attach(std::shared_ptr<Observer> observer) override {
observers.push_back(observer);
}
void detach(std::shared_ptr<Observer> observer) override {
for (auto it = observers.begin(); it != observers.end();) {
if (auto shared_obs = it->lock()) {
if (shared_obs == observer) {
it = observers.erase(it);
} else {
++it;
}
} else {
// 自动清理已销毁的过期观察者
it = observers.erase(it);
}
}
}
void notify() override {
for (auto& weak_obs : observers) {
// 升级为shared_ptr,确保对象存活时才调用更新
if (auto shared_obs = weak_obs.lock()) {
shared_obs->update(message);
}
}
}
void publishMessage(const std::string& msg) {
message = msg;
std::cout << "【安全发布器】发布:" << msg << std::endl;
notify();
}
};
// 具体观察者
class SafeUser : public Observer {
private:
std::string name;
public:
explicit SafeUser(std::string n) : name(std::move(n)) {}
void update(const std::string& state) override {
std::cout << "【用户" << name << "】收到:" << state << std::endl;
}
};
// 客户端代码
int main() {
auto publisher = std::make_shared<SafePublisher>();
auto user1 = std::make_shared<SafeUser>("张三");
auto user2 = std::make_shared<SafeUser>("李四");
publisher->attach(user1);
publisher->attach(user2);
publisher->publishMessage("新版本已上线");
// 观察者主动销毁,主题自动感知,不会产生野指针
user2.reset();
std::cout << "\n--- 用户李四已销毁 ---" << std::endl;
publisher->publishMessage("紧急修复包已推送");
return 0;
}
四、观察者模式的优缺点
优点
- 解耦性强 主题与观察者仅依赖抽象接口,互不感知具体实现。新增观察者类型无需修改主题代码,完全符合开闭原则。
- 动态可扩展 可在运行时动态添加、移除观察者,灵活调整通知链路,适配多变的业务需求。
- 一对多广播能力 支持一个主题同时通知任意数量的观察者,主题无需关心观察者的业务逻辑,只需负责状态广播。
缺点
- 性能随观察者数量衰减 如果观察者数量庞大,遍历通知的过程会产生明显的性能开销;复杂场景下需要配合异步队列优化。
- 通知顺序不可控 默认实现中,观察者的执行顺序依赖列表存储顺序,无法灵活指定优先级。
- 循环调用风险 如果观察者同时也是主题,互相订阅容易形成调用闭环,导致程序死循环。
- 信息粒度两难 推模式下主题推送全量状态,观察者可能不需要全部信息;拉模式下观察者主动获取状态,又会增加耦合度。
五、典型应用场景
观察者模式在各类软件系统中无处不在,典型落地场景包括:
- GUI 图形界面 几乎所有 UI 框架的事件机制(按钮点击、窗口缩放、键盘输入)都基于观察者模式:控件是主题,业务逻辑是观察者,用户操作触发事件通知。
- 消息中间件与事件总线 MQ 的发布 - 订阅模型、后端系统的事件总线、前端的 EventEmitter,本质都是观察者模式的工程化扩展。
- 架构分层解耦 MVC/MVVM 架构中,数据模型(Model)是主题,视图(View)是观察者,数据变化后自动触发视图刷新。
- 监控告警系统 监控指标(主题)超过阈值时,自动通知短信、邮件、办公软件等多个告警渠道(观察者)。
- 分布式配置中心 配置更新后,所有服务实例自动拉取最新配置,无需人工重启或轮询。
六、总结
观察者模式的核心价值,在于用「抽象依赖」替代「硬编码依赖」,让一对多的对象联动变得灵活、可扩展。它不是银弹,在简单场景下过度使用会增加代码复杂度,但在需要多对象联动、解耦发布与订阅的场景中,它是最经典也最高效的解决方案。
在 C++ 开发中使用观察者模式时,尤其要注意对象生命周期管理,结合weak_ptr实现安全的弱引用存储,是避免内存泄漏和野指针的最佳实践。