基础
观察者模式(Observer Pattern)是一种设计模式,定义对象间的一对多依赖关系。当一个对象的状态发生改变时,它的所有依赖者(观察者)都会收到通知并自动更新。这个模式常用于事件处理系统,如GUI事件、通知机制等。
观察者模式的关键组成部分
- 主题(Subject):被观察的对象,拥有维护和管理观察者的能力,可以在其状态发生变化时通知观察者。
- 观察者(Observer):当主题的状态发生变化时,接收通知并进行相应的更新。
- 具体主题(Concrete Subject):继承自主题,具体实现主题的功能。
- 具体观察者(Concrete Observer):继承自观察者,具体实现更新逻辑。
C++实现观察者模式
1. 定义观察者接口
cpp
#include <iostream>
#include <vector>
#include <string>
#include <memory> // 引入智能指针库
// 定义抽象观察者类
class Observer {
public:
virtual ~Observer() {}
virtual void update(const std::string& message_from_subject) = 0;
};
在这里,我们定义了一个纯虚函数 update()
,表示所有具体观察者都必须实现的接口。
2. 定义主题接口
cpp
class Subject {
public:
virtual ~Subject() {}
virtual void attach(std::shared_ptr<Observer> observer) = 0; // 添加观察者
virtual void detach(std::shared_ptr<Observer> observer) = 0; // 移除观察者
virtual void notify() = 0; // 通知观察者
};
主题接口提供了 attach()
、detach()
和 notify()
方法,用于添加、移除和通知观察者。
3. 具体主题实现
cpp
class ConcreteSubject : public Subject {
private:
std::vector<std::shared_ptr<Observer>> observers; // 维护一个观察者列表
std::string message;
public:
// 添加观察者
void attach(std::shared_ptr<Observer> observer) override {
observers.push_back(observer);
}
// 移除观察者
void detach(std::shared_ptr<Observer> observer) override {
observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end());
}
// 通知所有观察者
void notify() override {
for (auto observer : observers) {
observer->update(message);
}
}
// 设置状态,并通知观察者
void createMessage(const std::string& message) {
this->message = message;
notify();
}
};
在 ConcreteSubject
中,我们维护一个观察者的列表。当主题的状态变化时,会通过 notify()
方法通知所有观察者。
4. 具体观察者实现
cpp
class ConcreteObserver : public Observer {
private:
std::string name; // 观察者名称
std::string message_from_subject;
public:
ConcreteObserver(const std::string& name) : name(name) {}
void update(const std::string& message_from_subject) override {
this->message_from_subject = message_from_subject;
display();
}
void display() const {
std::cout << "Observer [" << name << "] received message: " << message_from_subject << std::endl;
}
};
具体观察者类实现了 update()
方法,每当接收到来自主题的消息时,它会更新其状态并显示消息。
5. 测试观察者模式
cpp
int main() {
auto subject = std::make_shared<ConcreteSubject>();
auto observer1 = std::make_shared<ConcreteObserver>("Observer 1");
auto observer2 = std::make_shared<ConcreteObserver>("Observer 2");
auto observer3 = std::make_shared<ConcreteObserver>("Observer 3");
subject->attach(observer1);
subject->attach(observer2);
subject->attach(observer3);
subject->createMessage("Hello Observers!");
subject->detach(observer2); // 移除Observer 2
subject->createMessage("New Update Available!");
return 0;
}
代码解释
- 创建主题和观察者 :
ConcreteSubject
实例化后,我们创建了多个观察者对象(ConcreteObserver
)。 - 注册观察者 :通过
attach()
方法将观察者注册到主题上。 - 通知观察者 :当主题的状态通过
createMessage()
方法改变时,它会调用notify()
方法通知所有的观察者。 - 移除观察者 :使用
detach()
方法,可以移除不再需要通知的观察者。 - 观察者接收通知 :每个观察者在接收到通知后,通过
update()
方法更新自己并输出消息。
运行结果
Observer [Observer 1] received message: Hello Observers!
Observer [Observer 2] received message: Hello Observers!
Observer [Observer 3] received message: Hello Observers!
Observer [Observer 1] received message: New Update Available!
Observer [Observer 3] received message: New Update Available!
小结
- 观察者模式有助于实现解耦,当一个对象状态发生改变时,会自动通知其依赖的对象,而不需要显式调用它们。
- 在实际应用中,常用于实现事件通知机制、订阅-发布模式等场景。
改进
代码还可以从以下几个方面进行改进,提升代码的健壮性、灵活性和性能:
1. 使用 std::weak_ptr
避免循环引用
在观察者模式中,如果 Subject
和 Observer
之间的关系过于复杂,且双方都使用 std::shared_ptr
彼此引用,可能会导致循环引用(也称为"循环依赖"),使得智能指针无法释放内存,导致内存泄漏。
改进措施:使用 std::weak_ptr
打破循环引用。std::weak_ptr
不增加引用计数,它只持有对象的弱引用。
在这个例子中,Subject
持有观察者的 std::weak_ptr
,而观察者依旧可以使用 std::shared_ptr
。
cpp
#include <iostream>
#include <vector>
#include <string>
#include <memory>
class Observer {
public:
virtual ~Observer() {}
virtual void update(const std::string& message_from_subject) = 0;
};
class Subject {
public:
virtual ~Subject() {}
virtual void attach(std::shared_ptr<Observer> observer) = 0;
virtual void detach(std::shared_ptr<Observer> observer) = 0;
virtual void notify() = 0;
};
class ConcreteSubject : public Subject {
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 {
observers.erase(
std::remove_if(observers.begin(), observers.end(),
[&observer](const std::weak_ptr<Observer>& o) {
return o.lock() == observer;
}),
observers.end());
}
void notify() override {
for (auto it = observers.begin(); it != observers.end();) {
if (auto observer = it->lock()) { // lock() 将 weak_ptr 转换为 shared_ptr
observer->update(message);
++it;
} else {
it = observers.erase(it); // 如果对象已被销毁,移除观察者
}
}
}
void createMessage(const std::string& message) {
this->message = message;
notify();
}
};
class ConcreteObserver : public Observer {
private:
std::string name;
std::string message_from_subject;
public:
ConcreteObserver(const std::string& name) : name(name) {}
void update(const std::string& message_from_subject) override {
this->message_from_subject = message_from_subject;
display();
}
void display() const {
std::cout << "Observer [" << name << "] received message: " << message_from_subject << std::endl;
}
};
int main() {
auto subject = std::make_shared<ConcreteSubject>();
auto observer1 = std::make_shared<ConcreteObserver>("Observer 1");
auto observer2 = std::make_shared<ConcreteObserver>("Observer 2");
auto observer3 = std::make_shared<ConcreteObserver>("Observer 3");
subject->attach(observer1);
subject->attach(observer2);
subject->attach(observer3);
subject->createMessage("Hello Observers!");
subject->detach(observer2); // 移除Observer 2
subject->createMessage("New Update Available!");
return 0;
}
改进要点:
std::weak_ptr
用于Subject
持有的观察者列表,避免循环引用和内存泄漏。- 通过
weak_ptr.lock()
检查对象是否仍然有效,如果观察者已被销毁,将其从列表中移除。
2. 性能优化
- 延迟通知 :在有大量观察者时,通知所有观察者可能会带来性能开销。如果对实时性要求不高,可以引入批处理 或者异步通知 机制。可以使用 C++ 标准库中的
std::thread
或std::async
来实现通知的并行或异步操作。
3. 增强可扩展性
- 事件过滤 :当前的观察者模式是所有观察者对每个通知都作出响应。可以通过在
notify
方法中增加条件逻辑,使观察者只对某些特定的事件类型作出响应,从而减少不必要的通知。例如,可以引入事件枚举,判断是否应该通知某个观察者。
cpp
enum class EventType {
MESSAGE_UPDATE,
DATA_UPDATE
};
class Observer {
public:
virtual ~Observer() {}
virtual void update(EventType type, const std::string& message) = 0;
};
class ConcreteSubject : public Subject {
// 略...
void notify(EventType type) override {
for (auto observer : observers) {
if (auto o = observer.lock()) {
o->update(type, message);
}
}
}
// 可以根据不同的事件类型发送不同的通知
void createMessage(const std::string& message) {
this->message = message;
notify(EventType::MESSAGE_UPDATE);
}
};
改进要点:
- 添加事件类型判断,通知相关的观察者,避免不必要的更新,提升效率。
4. 线程安全
如果你的项目是多线程环境,考虑将 Subject
的 attach()
、detach()
和 notify()
方法添加互斥锁以保证线程安全,避免多线程同时修改观察者列表可能带来的数据竞争。
可以使用 C++11 提供的 std::mutex
实现线程安全:
cpp
#include <mutex>
class ConcreteSubject : public Subject {
private:
std::vector<std::weak_ptr<Observer>> observers;
std::string message;
std::mutex mtx; // 互斥锁保证线程安全
public:
void attach(std::shared_ptr<Observer> observer) override {
std::lock_guard<std::mutex> lock(mtx); // 加锁
observers.push_back(observer);
}
void detach(std::shared_ptr<Observer> observer) override {
std::lock_guard<std::mutex> lock(mtx); // 加锁
observers.erase(
std::remove_if(observers.begin(), observers.end(),
[&observer](const std::weak_ptr<Observer>& o) {
return o.lock() == observer;
}),
observers.end());
}
void notify() override {
std::lock_guard<std::mutex> lock(mtx); // 加锁
for (auto it = observers.begin(); it != observers.end();) {
if (auto observer = it->lock()) {
observer->update(message);
++it;
} else {
it = observers.erase(it);
}
}
}
// 其他方法略...
};
改进要点:
- 引入互斥锁确保观察者列表的修改和遍历在多线程下的安全性。
5. 日志功能
可以为观察者模式中的 notify()
添加日志功能,记录每次的通知行为和状态变化。借助 std::ofstream
或其他日志库(如 spdlog
),可以方便地跟踪整个系统的事件传播过程。
总结:
- 使用
std::weak_ptr
避免循环引用。 - 引入 事件类型 过滤以减少不必要的通知。
- 增加 线程安全机制 以支持多线程环境。
- 考虑异步或并行的 性能优化。
- 添加日志以增强 调试能力。
通过这些改进,代码将变得更加健壮、可扩展且高效。
拓展
观察者模式有许多变种和扩展,常见的变种主要针对不同应用场景下的需求,增强了模式的灵活性、性能和可维护性。下面详细讲解几种常见的观察者模式变种,以及每种变种适用的场景和优势。
1. 推模型 vs 拉模型
这是最经典的观察者模式的变体,主要区别在于通知更新时数据的传递方式。
推模型(Push Model)
在推模型中,主题(Subject)主动将全部数据或状态变化直接推送 给所有观察者,不需要观察者自己去请求数据。也就是说,主题在通知观察者时,将所有相关数据作为参数传递给观察者的 update()
方法。
优点:
- 简化了观察者的设计,观察者只需要处理接收到的数据,无需关心如何获取。
缺点:
- 如果数据量大而观察者并不需要全部数据,可能造成不必要的性能开销。
实现示例:
cpp
void notify() override {
for (auto observer : observers) {
if (auto o = observer.lock()) {
o->update(message); // 推送所有的message内容
}
}
}
拉模型(Pull Model)
在拉模型中,观察者主动从主题中拉取数据。当主题状态变化时,它只通知观察者,具体数据由观察者调用主题的接口来获取。
优点:
- 观察者可以只获取自己关心的部分数据,减少数据传输的冗余。
- 提高了灵活性,主题只负责通知,数据由观察者自己决定是否需要获取。
缺点:
- 观察者和主题耦合度略高,观察者需要知道主题的状态获取接口。
实现示例:
cpp
void notify() override {
for (auto observer : observers) {
if (auto o = observer.lock()) {
o->update(); // 只通知有变化,不传递具体数据
}
}
}
class ConcreteObserver : public Observer {
private:
std::shared_ptr<ConcreteSubject> subject; // 观察者保存对主题的引用
public:
void update() override {
std::string data = subject->getMessage(); // 从主题拉取数据
std::cout << "Received: " << data << std::endl;
}
};
2. 同步观察者模式 vs 异步观察者模式
同步观察者模式
这是经典的观察者模式实现,所有观察者的通知是同步进行的 ,也就是说,当主题的 notify()
方法调用时,主题会逐一调用每个观察者的 update()
方法。所有观察者必须在 notify()
调用结束前完成更新。
优点:
- 实现简单,适合对实时性要求较高的场景。
- 主题在所有观察者都完成更新之前,不会继续其他任务。
缺点:
- 如果某个观察者执行时间较长,可能会拖慢整个系统的响应速度。
异步观察者模式
在异步观察者模式中,主题和观察者之间的通知是异步的,通常通过多线程或事件驱动机制来实现。主题只负责发布事件或通知,而观察者在后台线程或通过消息队列处理更新。
优点:
- 提高了系统的响应性,主题不必等待所有观察者完成更新。
- 适用于观察者更新任务较重或者延迟不敏感的场景。
缺点:
- 由于异步执行,可能会产生竞态条件,需要额外的同步机制保证数据一致性。
- 调试复杂,观察者收到通知的顺序可能与实际发生顺序不同。
异步实现示例 :
可以使用 C++11 的 std::async
或 std::thread
实现异步通知。
cpp
#include <future>
void notify() override {
for (auto observer : observers) {
if (auto o = observer.lock()) {
std::async(std::launch::async, [o]() {
o->update(); // 异步调用update
});
}
}
}
3. 事件总线(Event Bus)/ 发布-订阅模式(Publish-Subscribe Pattern)
发布-订阅模式 是观察者模式的一种更灵活的扩展。在发布-订阅模式中,发布者和订阅者之间没有直接的引用关系,它们通过中间消息通道 或事件总线通信。发布者发布事件,订阅者订阅某类事件,事件总线负责路由消息。
这种模式比观察者模式更加解耦,订阅者可以动态订阅某类事件,发布者也无需知道订阅者的存在。
优点:
- 高度解耦:发布者和订阅者之间没有直接依赖,任何订阅者都可以订阅某个事件。
- 灵活性强:支持多种事件类型,订阅者可以动态订阅和取消订阅。
缺点:
- 性能开销大:在大规模系统中,事件路由和消息传递可能会带来性能问题。
- 调试复杂:由于异步和分布式的特性,调试事件流的路径和问题变得更加困难。
事件总线示例:
cpp
#include <unordered_map>
#include <vector>
#include <functional>
class EventBus {
private:
std::unordered_map<int, std::vector<std::function<void()>>> subscribers; // 事件ID和处理函数映射
public:
void subscribe(int eventId, std::function<void()> handler) {
subscribers[eventId].push_back(handler);
}
void publish(int eventId) {
if (subscribers.find(eventId) != subscribers.end()) {
for (auto& handler : subscribers[eventId]) {
handler(); // 通知所有订阅者
}
}
}
};
发布-订阅场景:
- 发布者调用
EventBus.publish(eventId)
发布事件。 - 订阅者使用
EventBus.subscribe(eventId, handler)
动态订阅感兴趣的事件。
4. 双向观察者模式(Bidirectional Observer Pattern)
在某些场景中,观察者与被观察者之间的依赖关系是双向的。例如,两个对象彼此依赖,A 是 B 的观察者,B 也是 A 的观察者。在这种情况下,可能会引发无限循环更新(因为双方会不断通知对方)。
为了解决这种问题,双向观察者模式 通过标记机制避免循环通知。例如,当一个对象已经被更新时,它会标记自己,避免重复通知对方。
实现思路:
- 增加一个标志位或状态变量,表示该对象是否已经被更新。
- 在通知时先检查该标志位,防止重复更新。
cpp
class BidirectionalObserver : public Observer {
private:
bool isUpdated = false; // 标志位
public:
void update() override {
if (!isUpdated) {
isUpdated = true;
// 执行更新操作
}
}
void reset() {
isUpdated = false; // 重置状态
}
};
5. 层次观察者模式(Hierarchical Observer Pattern)
在某些复杂系统中,观察者可能有层次结构。例如,一个组件的变化会触发其下级组件的变化。层次观察者模式允许在父子关系中传播事件,即一个事件从父级传播到所有子级,类似于 GUI 系统中的事件冒泡机制。
实现思路:
- 主题(Subject)可以有子主题,每个主题通知它的观察者时,也会通知其子主题。
- 子主题再继续通知它的观察者,直到层次结构的底端。
示例:
cpp
class HierarchicalSubject : public Subject {
private:
std::vector<std::shared_ptr<HierarchicalSubject>> children;
public:
void addChild(std::shared_ptr<HierarchicalSubject> child) {
children.push_back(child);
}
void notify() override {
Subject::notify(); // 先通知自身的观察者
for (auto& child : children) {
child->notify(); // 递归通知子主题
}
}
};
总结:
- 推模型 vs 拉模型:通过不同的数据传递方式优化性能和灵活性。
- 同步 vs 异步:同步提供简单实时性,而异步提高系统响应能力,适合并发环境。
- 发布-订阅模式:进一步解耦观察者与主题,通过事件总线实现灵活通信。
- 双向观察者模式:解决循环通知问题,适用于双向依赖的场景。
- 层次观察者模式:适合父子层级结构的事件传递机制,类似 GUI 事件冒泡。
这些变种在不同的应用场景中,可以让观察者模式更加灵活、强大,同时也更适应复杂系统的需求。