1. 引言:观察者模式简介(Introduction: A Brief Overview of Observer Pattern)
1.1. 什么是观察者模式(What is Observer Pattern)
📌观察者模式:本质为触发联动,定义对象间的一对多的依赖关系,以便 当一个对象(subject)状态发生了变化,所有依赖它的对象都得到通知并更新.
观察者模式(Observer Pattern)是一种行为型设计模式,主要用于在对象间建立一种一对多的依赖关系,使得一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。这种模式通常用于实现事件驱动系统,例如用户界面框架、数据同步等场景。
观察者模式主要包含两类对象:主题(Subject)和观察者(Observer)。主题是一个具有状态的对象,它的状态可以发生变化。观察者是依赖于主题的对象,当主题的状态发生变化时,观察者需要得到通知并作出相应的响应。在观察者模式中,主题和观察者之间的关系是松耦合的,这意味着它们可以独立地改变和复用,而不会相互影响。
在C++中实现观察者模式,可以使用多态性、继承和接口等特性。接下来的章节将详细介绍观察者模式的组成部分以及如何在C++中实现这种模式。
1.2. 观察者模式的适用场景(Appropriate Scenarios for Observer Pattern)
观察者模式适用于以下场景:
动态依赖关系:当一个对象需要动态地通知其他对象其状态变化时,观察者模式可以提供一种灵活的解决方案。例如,一个数据模型需要通知多个视图更新其显示内容时,可以采用观察者模式。
松耦合:当希望实现一种松耦合的架构,以便在不影响其他对象的情况下独立修改、测试或重用某个对象时,观察者模式是一个很好的选择。由于主题和观察者之间的关系是松耦合的,因此它们可以独立地变化和发展。
广播通信:在需要广播通信的场景中,观察者模式可以有效地将消息传递给多个对象。例如,一个事件总线(Event Bus)可以使用观察者模式,让多个订阅者(观察者)接收到事件通知。
跨系统通信:在跨系统通信的场景中,观察者模式可以作为中间件,将消息从一个系统传递给另一个系统。例如,实时数据同步、消息队列等应用场景。
总之,观察者模式适用于在对象间建立动态依赖关系,实现松耦合架构以及广播通信等场景。在接下来的章节中,我们将详细介绍观察者模式的基本组成以及如何在C++中实现这种模式。
1.3为什么要使用观察者模式?
观察者模式和信号量、互斥锁等同步原语在解决问题时有一些重要区别。观察者模式主要解决的是对象间的一种松散耦合关系,而信号量和互斥锁主要用于解决多线程间的同步和互斥问题。
观察者模式的优势:
- 松散耦合:观察者模式允许您构建松散耦合的类关系。被观察者和观察者之间并不直接相互依赖,只需关注其接口,而不关心具体实现。这种关系使得组件之间的改变和扩展变得更加容易。
- 可扩展性:您可以根据需要向系统添加新的观察者,而无需对被观察者做任何修改。同样,可以方便地移除或替换观察者。
- 支持广播:被观察者可以同时通知多个观察者,当状态发生变化时,所有关注这个状态的观察者都会收到通知。
信号量和互斥锁的优势:
- 线程同步:信号量和互斥锁的主要目的是控制多线程访问共享资源的同步。它们保证了资源访问的互斥性和同步性,避免了多线程下的竞争条件和死锁。
- 易于理解:信号量和互斥锁的概念相对简单,容易理解,使用也较为直接。
从您的需求来看,使用观察者模式的原因是要在不同对象之间实现松散耦合的通知机制。使用信号量和互斥锁会导致更强的耦合,并且它们主要用于解决多线程同步问题,而非通知机制。
您也可以考虑直接引用的方式,但这种方式会导致耦合更紧密,可能不利于后续的代码维护和扩展。使用观察者模式能够在一定程度上解耦和增强代码的可扩展性。
总之,观察者模式和信号量、互斥锁分别解决了不同类型的问题。根据具体情况选择最合适的方法来实现功能。
2. 观察者模式的基本组成(Basic Components of Observer Pattern)
2.1.角色分配
- 抽象主题(Subject):该角色是一个抽象类或接口,定义了增加、删除、通知观察者对象的方法。
- 具体主题(ConcreteSubject):该角色继承或实现了抽象主题,定义了一个集合存入注册过的具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
- 抽象观察者(Observer):该角色是具体观察者的抽象类,定义了一个更新方法。
- 具体观察者(ConcrereObserver):该角色是具体的观察者对象,在得到具体主题更改通知时更新自身的状态。
观察者模式中的两个类 - ISubject
和 Observer
- 分别表示主题(Subject,也称为被观察者,Observable)和观察者。它们具有不同的职责,将它们分开有助于实现单一职责原则(SRP),即一个类应该只有一个引起它变化的原因。
在观察者模式中:
ISubject
(主题/被观察者)是一个接口,它提供注册、移除和通知观察者的方法。具体主题(例如FileMangent
)将实现这个接口。这样,当状态发生变化时,主题可以通知已注册的观察者。Observer
(观察者)也是一个接口,它提供一个update
方法,用于在被通知时更新观察者的状态。具体观察者(例如PlayMangent
)将实现这个接口。
将这两个角色分开的原因是:
- 它们有不同的职责:主题负责管理观察者并在状态发生变化时通知它们,观察者负责在收到通知时执行特定的操作。
- 它允许更大的灵活性:不同的主题可以通知不同的观察者,观察者可以根据需要订阅和取消订阅。
- 它促使代码更加模块化和可重用,因为主题和观察者的实现是相互独立的。
如果您将两个角色合并为一个类,那么这个类将承担太多职责,难以满足单一职责原则。同时,这种设计可能导致代码变得复杂且难以维护,降低了可重用性和灵活性。
2.2观察者模式UML图
观察者模式(Observer Pattern)是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象(被观察者)的状态发生改变时,所有依赖它的对象(观察者)都会自动收到通知并作出相应的更新。以下是一个观察者模式的UML图:
+----------------+ +---------------+ | <<interface>> | | <<interface>> | | Subject | | Observer | +----------------+ +---------------+ | +attach(Observer) : void | +update() : void | +detach(Observer) : void +---------------+ | +notifyObservers() : void | +----------------+ | ^ ^ | | +------------------+ +------------+ | ConcreteSubject| | ConcreteObserver| +------------------+ +------------+ | +getState() : StateType | +update() : void | +setState(StateType) : void +------------+ +------------------+ |
在这个UML图中:
- Subject(被观察者)接口:定义了添加、删除和通知观察者的方法。具体的被观察者(ConcreteSubject)会实现这个接口。
- Observer(观察者)接口:定义了一个更新方法,用于在被观察者状态发生改变时进行相应的操作。具体的观察者(ConcreteObserver)会实现这个接口。
- ConcreteSubject(具体被观察者):实现了Subject接口的具体类,维护一个观察者列表,并在其状态发生变化时通知所有观察者。
- ConcreteObserver(具体观察者):实现了Observer接口的具体类,根据被观察者的状态改变来更新自身状态。
2.3. 主题(Subject)
主题(Subject)是一个具有状态的对象,它的状态可以发生变化。当主题的状态发生变化时,需要通知所有依赖于它的观察者。主题的接口通常包含以下方法:
attach(Observer* o):用于注册一个观察者。
detach(Observer* o):用于注销一个观察者。
notify():用于通知所有已注册的观察者状态变化。
在C++中,可以定义一个抽象基类来表示主题接口:
class Subject { public: virtual void attach(Observer* o) = 0; virtual void detach(Observer* o) = 0; virtual void notify() = 0; };
2.4. 观察者(Observer)
观察者(Observer)是依赖于主题的对象,当主题的状态发生变化时,观察者需要得到通知并作出相应的响应。观察者的接口通常包含以下方法:
update(Subject* s):当主题状态发生变化时,观察者通过此方法得到通知。参数s表示发生变化的主题,观察者可以通过访问主题的状态来更新自己的状态。
在C++中,可以定义一个抽象基类来表示观察者接口:
class Observer { public: virtual void update(Subject* s) = 0; };
在观察者模式中,主题和观察者的接口定义了它们之间的通信协议。这种设计允许在不影响其他组件的情况下独立修改、测试或重用主题和观察者。接下来的章节将介绍如何实现具体主题和观察者。
2.5. 具体主题(Concrete Subject)
具体主题(Concrete Subject)是主题接口的实现类,它包含了状态以及实现了主题接口的方法。具体主题需要维护一个观察者列表,以便在状态发生变化时通知所有注册的观察者。以下是一个C++中的具体主题示例:
#include <vector> #include "Subject.h" #include "Observer.h" class ConcreteSubject : public Subject { public: void attach(Observer* o) override { observers.push_back(o); } void detach(Observer* o) override { observers.erase(std::remove(observers.begin(), observers.end(), o), observers.end()); } void notify() override { for (auto& observer : observers) { observer->update(this); } } int getState() const { return state; } void setState(int newState) { state = newState; notify(); } private: int state; std::vector<Observer*> observers; };
2.6. 具体观察者(Concrete Observer)
具体观察者(Concrete Observer)是观察者接口的实现类,它依赖于具体主题的状态。当具体主题的状态发生变化时,具体观察者需要更新自己的状态。以下是一个C++中的具体观察者示例:
#include <iostream> #include "Observer.h" #include "ConcreteSubject.h" class ConcreteObserver : public Observer { public: ConcreteObserver(ConcreteSubject* s) : subject(s) {} void update(Subject* s) override { if (s == subject) { observerState = subject->getState(); display(); } } void display() { std::cout << "Observer state: " << observerState << std::endl; } private: int observerState; ConcreteSubject* subject; };
在这个例子中,具体观察者通过继承观察者接口并实现update方法来实现自己的逻辑。当观察者收到具体主题的通知时,它会更新自己的状态并显示新状态。
通过组合具体主题和具体观察者,可以实现一个完整的观察者模式。在接下来的章节中,我们将详细介绍如何在C++中实现观察者模式。
3. C++实现观察者模式(Implementing Observer Pattern in C++)
3.1. 设计抽象类(Designing Abstract Classes)
在C++中实现观察者模式时,首先需要设计主题(Subject)和观察者(Observer)的抽象类。这些抽象类定义了主题和观察者之间的通信协议。
Subject抽象类:
class Subject { public: virtual void attach(Observer* o) = 0; virtual void detach(Observer* o) = 0; virtual void notify() = 0; };
Observer抽象类:
class Observer { public: virtual void update(Subject* s) = 0; };
在这里,Subject抽象类定义了attach、detach和notify方法,用于管理观察者以及通知它们状态变化。Observer抽象类定义了一个update方法,用于接收主题状态变化的通知。
这些抽象类为具体主题和具体观察者提供了基本的框架,接下来的章节将介绍如何实现具体主题和观察者。
3.2. 实现具体主题和观察者(Implementing Concrete Subjects and Observers)
在设计了主题和观察者的抽象类之后,接下来需要实现具体主题和观察者类。这些具体类继承自相应的抽象类,并实现了抽象类中定义的方法。
ConcreteSubject类:
#include <vector> #include <algorithm> #include "Subject.h" #include "Observer.h" class ConcreteSubject : public Subject { public: void attach(Observer* o) override { observers.push_back(o); } void detach(Observer* o) override { observers.erase(std::remove(observers.begin(), observers.end(), o), observers.end()); } void notify() override { for (auto& observer : observers) { observer->update(this); } } int getState() const { return state; } void setState(int newState) { state = newState; notify(); } private: int state; std::vector<Observer*> observers; };
ConcreteObserver类:
#include <iostream> #include "Observer.h" #include "ConcreteSubject.h" class ConcreteObserver : public Observer { public: ConcreteObserver(ConcreteSubject* s) : subject(s) {} void update(Subject* s) override { if (s == subject) { observerState = subject->getState(); display(); } } void display() { std::cout << "Observer state: " << observerState << std::endl; } private: int observerState; ConcreteSubject* subject; };
在这些实现中,ConcreteSubject类维护了一个观察者列表,并实现了attach、detach和notify方法。ConcreteObserver类则实现了update方法,用于接收主题状态变化的通知。当观察者收到具体主题的通知时,它会更新自己的状态并显示新状态。
通过实现具体主题和具体观察者,我们已经完成了观察者模式的核心部分。在接下来的章节中,我们将演示如何使用观察者模式。
3.3. 使用观察者模式(Using the Observer Pattern)
现在我们已经实现了具体主题和具体观察者,接下来将展示如何在实际应用中使用观察者模式。
main.:
#include "ConcreteSubject.h" #include "ConcreteObserver.h" int main() { // 创建一个具体主题 ConcreteSubject concreteSubject; // 创建两个具体观察者,并将它们附加到具体主题 ConcreteObserver observer1(&concreteSubject); ConcreteObserver observer2(&concreteSubject); concreteSubject.attach(&observer1); concreteSubject.attach(&observer2); // 更改具体主题的状态,并通知观察者 concreteSubject.setState(42); // 移除一个观察者,并更改具体主题的状态 concreteSubject.detach(&observer1); concreteSubject.setState(84); return 0; }
在这个示例中,我们首先创建了一个具体主题concreteSubject。接着,创建了两个具体观察者observer1和observer2,并将它们附加到具体主题。然后,我们更改了具体主题的状态,并通知了观察者。最后,我们移除了一个观察者,并再次更改了具体主题的状态。
当运行这个程序时,将会看到以下输出:
Observer state: 42 Observer state: 42 Observer state: 84
从输出中可以看出,当主题状态发生变化时,所有附加的观察者都会收到通知并更新自己的状态。当我们移除了一个观察者后,它不再接收主题状态变化的通知。
通过这个示例,我们可以看到观察者模式在实际应用中的作用。观察者模式提供了一种有效的方法来在对象之间建立一种一对多的依赖关系,以便在对象状态发生变化时通知其他对象。
3.4观察者模式中的数据结构选择
std::unordered_set
和 std::vector>
都可以用来实现观察者模式。它们分别使用哈希表和动态数组作为底层数据结构来存储观察者对象,因此在使用和性能上存在一些差异。以下是两者之间的优缺点对比:
std::unordered_set:
优点:
- 无重复元素:
std::unordered_set
保证其中的元素唯一,避免添加重复的观察者对象。 - 快速查找:
std::unordered_set
的查找、插入和删除操作时间复杂度接近 O(1),当观察者数量较大时,性能较好。
缺点:
- 原生指针:使用原生指针管理资源容易导致内存泄漏或悬挂指针。需要额外小心处理对象的生命周期。
- 无序:
std::unordered_set
中的元素是无序的,可能导致通知顺序不确定。
std::vector>:
优点:
- 顺序:
std::vector
中的元素保持插入顺序,确保观察者按顺序接收通知。 - 智能指针:使用
std::shared_ptr
管理对象生命周期,减少了内存泄漏和悬挂指针的风险。
缺点:
- 可能存在重复元素:
std::vector
不保证元素的唯一性,需要在添加观察者时检查是否已存在相同的观察者对象。 - 查找、插入和删除操作的时间复杂度为 O(n):在观察者数量较大时,性能可能较差。
根据具体需求和使用场景,您可以根据这些优缺点选择合适的数据结构来实现观察者模式。如果要求快速查找、插入和删除操作,并且能处理好原生指针的生命周期问题,std::unordered_set
可能是更好的选择。如果希望保持观察者的通知顺序,并且不太关注性能问题,std::vector>
可能更适合。
4. 观察者模式的优缺点(Pros and Cons of Observer Pattern)
4.1. 优点(Pros)
- 耦合:观察者模式可以实现主题和观察者之间的松耦合关系,这使得它们可以独立地修改、测试和重用,而不影响其他组件。
- 态依赖管理:观察者模式允许在运行时动态地添加或删除观察者,这为管理对象之间的依赖关系提供了更大的灵活性。-
- 播通信:观察者模式可以实现一对多的广播通信,使得当一个对象的状态发生变化时,可以通知多个依赖于它的对象。
- 于扩展:观察者模式易于扩展新的观察者,因为不需要修改主题的代码。新增的观察者只需实现观察者接口,并将自己注册到主题即可。
4.2. 缺点(Cons)
- 性能开销:在某些情况下,通知所有观察者可能会导致较大的性能开销,尤其是在大量观察者和频繁状态变化的场景中。
- 顺序问题:观察者模式并不能保证通知观察者的顺序,这可能导致某些观察者在其他观察者之前收到通知,从而引发潜在的问题。
- 以追踪调试:由于观察者模式的松耦合特性,有时可能难以追踪和调试观察者之间的交互。
尽管观察者模式具有一定的缺点,但它在很多情况下仍然是一种非常有用的设计模式。通过权衡观察者模式的优缺点,可以在适当的场景中选择并应用它。
5. 观察者模式的应用场景(Observer Pattern Use Cases)
一个系统内有许多存在耦合关系的对象,对象之间存在某种联动关系.
- ** 对一个对象状态的更新,需要其他对象同步更新,而且其他对象的数量动态可变。**
- ** 对象仅需要将自己的更新通知给其他对象而不需要知道其他对象的细节。** dd
5.1. 事件驱动系统(Event-driven Systems)
在事件驱动系统中,观察者模式是一种非常合适的设计模式。在这类系统中,许多组件需要在某个事件发生时被通知并作出相应的响应。这些事件可以是用户操作、系统状态变化或其他组件的行为。观察者模式可以在这些组件之间建立动态的依赖关系,使得它们在事件发生时能够相互通知。
5.2. 数据绑定和更新(Data Binding and Updates)
观察者模式可以用于实现数据绑定和自动更新。在这种场景下,通常有一个数据源(主题),以及许多依赖于此数据源的组件(观察者)。当数据源发生变化时,观察者模式可以确保所有依赖于该数据源的组件都能够自动更新,以反映新的数据状态。
5.3. 分布式系统(Distributed Systems)
在分布式系统中,观察者模式可以用于实现跨组件的通信和协作。通过将观察者模式与消息传递、远程过程调用等技术相结合,可以实现分布式系统中组件之间的松耦合协作。
5.4. 订阅发布系统(Publish-Subscribe Systems)
观察者模式是订阅发布系统的基础。在这种系统中,发布者(主题)会发送消息给订阅者(观察者),订阅者可以订阅感兴趣的主题,并在主题发送消息时接收到通知。这种模式适用于很多场景,如消息队列、事件总线等。
总之,观察者模式在很多场景下都是非常有用的设计模式。通过了解观察者模式的应用场景,可以更好地理解和应用这个模式。
6. C++11/14/17中的观察者模式(Observer Pattern in C++11/14/17)
C++11/14/17引入了许多新特性,可以帮助我们更简洁和安全地实现观察者模式。本章将介绍如何在C++11/14/17中应用观察者模式,特别是如何使用智能指针来管理观察者的生命周期。
6.1. 智能指针和观察者模式(Smart Pointers and Observer Pattern)
在C++11/14/17中,智能指针可以用来自动管理对象的生命周期。这可以避免手动管理内存分配和释放的复杂性,从而减少内存泄漏和悬挂指针等问题。在观察者模式中,我们可以使用智能指针来管理观察者的生命周期。
Subject抽象类:
#include <memory> #include <vector> #include <algorithm> class Observer; class Subject { public: void attach(std::shared_ptr<Observer> o); void detach(std::shared_ptr<Observer> o); void notify(); private: std::vector<std::weak_ptr<Observer>> observers; };
在这个实现中,我们使用std::shared_ptr来表示观察者的指针,并将观察者存储在一个std::weak_ptr的向量中。这样,当观察者的引用计数降为零时,观察者对象会被自动释放,而不会导致内存泄漏。同时,std::weak_ptr可以避免循环引用问题。
ConcreteSubject类:
#include "Subject.h" #include "Observer.h" class ConcreteSubject : public Subject { public: void attach(std::shared_ptr<Observer> o) { observers.push_back(o); } void detach(std::shared_ptr<Observer> o) { observers.erase(std::remove(observers.begin(), observers.end(), o), observers.end()); } void notify() { for (auto it = observers.begin(); it != observers.end();) { if (auto observer = it->lock()) { observer->update(this); ++it; } else { // Remove the expired weak_ptr it = observers.erase(it); } } } int getState() const { return state; } void setState(int newState) { state = newState; notify(); } private: int state; std::vector<std::weak_ptr<Observer>> observers; };
在这个实现中,我们对attach、detach和notify方法进行了相应的修改,以适应智能指针的使用。在notify方法中,我们使用std::weak_ptr的lock方法来获取观察者的std::shared_ptr。如果lock方法返回一个非空的std::shared_ptr,说明观察者对象仍然存在,可以使用。如果lock方法返回空指
6.2. Lambda表达式和观察者模式(Lambda Expressions and Observer Pattern)
在C++11/14/17中,我们可以使用Lambda表达式来创建匿名函数对象,从而简化代码并提高灵活性。在观察者模式中,我们可以使用Lambda表达式来简化观察者的实现。
首先,我们需要修改Observer类,使其可以接受一个Lambda表达式作为回调函数:
#include <functional> class Observer { public: Observer(std::function<void(Observer*, Subject*)> callback) : callback_(callback) {} void update(Subject* subject) { callback_(this, subject); } private: std::function<void(Observer*, Subject*)> callback_; };
在这个实现中,我们使用了std::function来表示一个可以接受两个参数的回调函数。在构造Observer对象时,我们可以传入一个Lambda表达式来实现具体的更新逻辑。
下面是一个使用Lambda表达式创建观察者的示例:
#include "ConcreteSubject.h" #include "Observer.h" int main() { // 创建一个具体主题 ConcreteSubject concreteSubject; // 创建一个具体观察者,并将其附加到具体主题 auto observer1 = std::make_shared<Observer>([](Observer* o, Subject* s) { if (auto cs = dynamic_cast<ConcreteSubject*>(s)) { std::cout << "Observer1 state: " << cs->getState() << std::endl; } }); auto observer2 = std::make_shared<Observer>([](Observer* o, Subject* s) { if (auto cs = dynamic_cast<ConcreteSubject*>(s)) { std::cout << "Observer2 state: " << cs->getState() << std::endl; } }); concreteSubject.attach(observer1); concreteSubject.attach(observer2); // 更改具体主题的状态,并通知观察者 concreteSubject.setState(42); // 移除一个观察者,并更改具体主题的状态 concreteSubject.detach(observer1); concreteSubject.setState(84); return 0; }
在这个示例中,我们使用了Lambda表达式来创建两个具体的观察者,并将它们附加到具体主题。当主题状态发生变化时,这些观察者会执行相应的Lambda表达式来更新自己的状态。
通过使用Lambda表达式,我们可以简化观察者的实现,并在创建观察者时灵活地定义更新逻辑。这使得观察者模式在C++11/14/17中更加易于使用和扩展。
7. 观察者模式的变体与扩展(Variants and Extensions of Observer Pattern)
7.1. 带通知功能的观察者模式(Observer Pattern with Notifications)
在标准的观察者模式中,当主题状态发生变化时,它会通知所有附加的观察者。然而,在某些情况下,我们可能希望通知观察者的同时,还能传递一些额外的信息。为了实现这个功能,我们可以对观察者模式进行扩展,使其支持带通知功能的观察者模式。
为了实现带通知功能的观察者模式,我们需要对Observer和Subject接口进行修改。首先,我们需要将通知类型作为模板参数添加到Observer接口:
template <typename Notification> class Observer { public: virtual ~Observer() = default; virtual void update(Subject* subject, const Notification& notification) = 0; };
接下来,我们需要修改Subject接口,以支持带通知的notify方法:
template <typename Notification> class Subject { public: virtual ~Subject() = default; virtual void attach(Observer<Notification>* observer) = 0; virtual void detach(Observer<Notification>* observer) = 0; virtual void notify(const Notification& notification) = 0; };
现在,我们可以创建一个带通知功能的具体主题,例如:
class ConcreteSubjectWithNotification : public Subject<std::string> { public: void attach(Observer<std::string>* observer) override { observers.push_back(observer); } void detach(Observer<std::string>* observer) override { observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end()); } void notify(const std::string& notification) override { for (auto& observer : observers) { observer->update(this, notification); } } private: std::vector<Observer<std::string>*> observers; };
在这个实现中,当我们调用notify方法时,可以传递一个通知(在本例中为std::string类型)。这使得观察者在收到通知时,可以根据附加的信息做出不同的响应。
带通知功能的观察者模式为主题和观察者之间的通信提供了更大的灵活性。在需要向观察者传递额外信息的场景下,这种扩展非常有用。
7.2. 双向观察者模式(Bidirectional Observer Pattern)
在传统的观察者模式中,主题和观察者之间的关系是单向的,即主题负责通知观察者,而观察者不能直接影响主题。然而,在某些情况下,我们可能需要实现双向通信,即观察者可以影响主题,或者主题可以请求观察者提供某些信息。为了实现这个功能,我们可以对观察者模式进行扩展,使其支持双向观察者模式。
为了实现双向观察者模式,我们首先需要对Observer和Subject接口进行修改,以支持双向通信:
class Observer; class Subject { public: virtual ~Subject() = default; virtual void attach(Observer* observer) = 0; virtual void detach(Observer* observer) = 0; virtual void notify() = 0; virtual void requestInfoFromObserver(Observer* observer) = 0; }; class Observer { public: virtual ~Observer() = default; virtual void update(Subject* subject) = 0; virtual void provideInfoToSubject(Subject* subject) = 0; };
在这个实现中,我们为Subject接口添加了一个新的方法requestInfoFromObserver,该方法允许主题向观察者请求信息。同时,我们为Observer接口添加了一个新的方法provideInfoToSubject,该方法允许观察者向主题提供信息。
下面是一个使用双向观察者模式的示例:
class ConcreteSubject : public Subject { public: // ... attach, detach, and notify methods ... void requestInfoFromObserver(Observer* observer) override { observer->provideInfoToSubject(this); } }; class ConcreteObserver : public Observer { public: // ... update method ... void provideInfoToSubject(Subject* subject) override { // Provide information to the subject } };
在这个示例中,当主题需要从观察者获取信息时,可以调用requestInfoFromObserver方法。观察者收到请求后,会执行provideInfoToSubject方法,并向主题提供所需的信息。
双向观察者模式为主题和观察者之间的通信提供了更大的灵活性。在需要实现双向通信的场景下,这种扩展非常有用。不过,需要注意的是,双向观察者模式可能会增加系统的复杂性,因此在使用时应谨慎评估是否确实需要这种扩展。
8. 观察者模式在实际项目中的应用(Real-life Applications of Observer Pattern)
8.1. GUI框架中的观察者模式(Observer Pattern in GUI Frameworks)
观察者模式在图形用户界面(GUI)框架中得到了广泛应用。在GUI框架中,通常需要在用户操作(如点击按钮、拖动滚动条等)或者程序状态改变时更新显示的内容。观察者模式提供了一种灵活的方式来实现这些需求。
在许多GUI框架中,观察者模式被用于实现事件监听和处理。在这种情况下,组件(如按钮、文本框等)充当主题,而用户自定义的事件处理器充当观察者。当组件状态发生变化时(例如,按钮被点击),组件会通知相应的事件处理器,然后事件处理器会根据需要更新显示内容或执行其他操作。
以下是一个使用Qt框架实现观察者模式的简单示例:
#include <QApplication> #include <QPushButton> #include <QMessageBox> int main(int argc, char* argv[]) { QApplication app(argc, argv); QPushButton button("Click me!"); QObject::connect(&button, &QPushButton::clicked, [&]() { QMessageBox::information(nullptr, "Message", "Button clicked!"); }); button.show(); return app.exec(); }
在这个示例中,我们创建了一个简单的Qt应用程序,其中包含一个按钮。我们使用QObject::connect方法将按钮的clicked信号连接到一个Lambda表达式,该表达式显示一个消息框。当用户点击按钮时,按钮会发出clicked信号,触发与之连接的Lambda表达式。
此示例展示了如何在GUI框架中使用观察者模式来实现事件监听和处理。使用观察者模式,我们可以轻松地将组件状态变化与相应的响应动作关联起来,从而简化GUI应用程序的开发。
8.2. 游戏开发中的观察者模式(Observer Pattern in Game Development)
观察者模式在游戏开发中也得到了广泛应用。游戏通常涉及许多交互式元素,这些元素需要在特定事件发生时(如角色升级、敌人被击败等)执行相应的操作。观察者模式提供了一种简单有效的方式来实现这些需求,让开发人员能够轻松地在游戏对象之间建立通信。
以下是一个使用观察者模式实现游戏成就系统的简单示例:
#include "Observer.h" #include "ConcreteSubject.h" class Achievement : public Observer { public: void update(Subject* subject) override { if (auto player = dynamic_cast<Player*>(subject)) { if (player->getLevel() >= 10) { std::cout << "Achievement unlocked: Level 10 reached!" << std::endl; } } } }; int main() { Player player; Achievement achievement; player.attach(&achievement); for (int i = 1; i <= 20; ++i) { player.setLevel(i); } return 0; }
在这个示例中,我们创建了一个游戏角色Player(实现自ConcreteSubject)和一个成就系统Achievement(实现自Observer)。当角色的等级达到10时,成就系统会解锁一个成就。为了实现这个功能,我们将Achievement对象附加到Player对象,然后在角色等级变化时更新成就系统。
通过使用观察者模式,我们可以将游戏对象的状态变化与相应的操作关联起来,从而简化游戏开发。观察者模式还有助于实现游戏对象之间的松散耦合,这使得游戏更容易扩展和维护。
9. 观察者模式的优缺点(Pros and Cons of Observer Pattern)
在了解了观察者模式及其在实际项目中的应用之后,让我们来总结一下它的优缺点。
9.1. 优点(Pros)
解耦:观察者模式将主题与观察者解耦,这意味着它们可以独立地发展和修改,而不会影响彼此。这有助于提高代码的可维护性和可扩展性。
动态关联:在运行时,主题可以动态地添加或删除观察者,这为实现复杂的系统提供了灵活性。
广播通信:观察者模式允许主题向所有附加的观察者广播通知,这使得在多个对象之间同步状态变得简单。
9.2. 缺点(Cons)
性能问题:如果观察者数量很大或者更新操作非常频繁,观察者模式可能导致性能问题。在这种情况下,使用其他方法,如事件队列或消息总线,可能是更合适的选择。
内存泄漏风险:如果在主题被销毁之前未将观察者从主题中分离,可能会导致内存泄漏。在实现观察者模式时,需要特别注意避免这种问题。
难以调试:由于观察者模式的动态特性,调试代码可能变得更加困难。当系统中存在大量观察者时,找到特定问题的来源可能需要更多的努力。
10.如何做到异步通知
默认情况下,观察者的回调是在被观察者线程执行.为了实现在观察者线程中执行回调,您需要采用上面讨论过的一些方法,如使用std::thread
、线程池、std::async
和std::future
或消息队列等。这些方法可以帮助您将回调的执行切换到观察者线程,而不是在被观察者线程中执行。
新开线程执行
要实现在观察者线程中执行回调,而不是在被观察者线程中执行回调,可以使用多线程和同步机制。具体来说,可以使用std::thread
和std::mutex
,std::condition_variable
等C++标准库提供的多线程和同步工具。
下面是一个简单的示例,展示如何在观察者线程中执行回调:
#include <iostream> #include <vector> #include <thread> #include <mutex> #include <condition_variable> #include <functional> class Observer; class Subject { public: void addObserver(Observer* observer); void removeObserver(Observer* observer); void notifyObservers(); protected: std::vector<Observer*> observers; }; class Observer { public: Observer(Subject* subject); virtual ~Observer() = default; virtual void update() = 0; protected: Subject* subject; }; void Subject::addObserver(Observer* observer) { observers.push_back(observer); } void Subject::removeObserver(Observer* observer) { observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end()); } void Subject::notifyObservers() { for (auto& observer : observers) { observer->update(); } } Observer::Observer(Subject* subject) : subject(subject) { subject->addObserver(this); } class ConcreteObserver : public Observer { public: ConcreteObserver(Subject* subject) : Observer(subject) {} void update() override; private: void callback(); }; void ConcreteObserver::update() { std::thread t(&ConcreteObserver::callback, this); t.detach(); } void ConcreteObserver::callback() { std::cout << "Callback executed in observer's thread." << std::endl; } class ConcreteSubject : public Subject { public: void someEvent() { std::cout << "An event occurred in the subject." << std::endl; notifyObservers(); } }; int main() { ConcreteSubject subject; ConcreteObserver observer1(&subject), observer2(&subject); subject.someEvent(); std::this_thread::sleep_for(std::chrono::seconds(1)); return 0; }
上述代码示例中,ConcreteObserver
类的update
方法会在一个新的线程中执行回调。这样,当被观察者通知观察者时,回调将在观察者线程中执行,而不是在被观察者线程中执行。
注意,当在多线程环境中使用观察者模式时,需要确保观察者和被观察者的状态同步,以避免竞争条件和数据不一致。在这种情况下,可以使用std::mutex
和std::unique_lock
等同步工具。
使用线程池(std::thread
的封装)
线程池可以有效地管理线程资源。当有任务需要执行时,线程池会从空闲线程中分配一个线程来执行任务。使用线程池可以降低创建和销毁线程的开销。C++没有内置的线程池,但您可以使用像ThreadPool
这样的开源库或者自己实现一个。
示例:
// 假设ThreadPool类已经实现 class ConcreteObserver : public Observer { public: ConcreteObserver(Subject* subject, ThreadPool& threadPool) : Observer(subject), threadPool(threadPool) {} void update() override; private: ThreadPool& threadPool; void callback(); }; void ConcreteObserver::update() { threadPool.enqueue(std::bind(&ConcreteObserver::callback, this)); } void ConcreteObserver::callback() { std::cout << "Callback executed in observer's thread." << std::endl; }
使用std::async
和std::future
std::async
是C++11中引入的一个函数,它可以异步执行函数,并返回一个std::future
对象,表示函数的返回值。您可以使用std::async
来异步地执行观察者的回调。
示例:
#include <future> class ConcreteObserver : public Observer { public: ConcreteObserver(Subject* subject) : Observer(subject) {} void update() override; private: void callback(); }; void ConcreteObserver::update() { std::future<void> future = std::async(std::launch::async, &ConcreteObserver::callback, this); } void ConcreteObserver::callback() { std::cout << "Callback executed in observer's thread." << std::endl; }
在这个例子中,std::async
将观察者的回调函数作为参数,创建一个std::future
对象。std::launch::async
策略指示std::async
在新线程中执行函数。请注意,std::future
对象在析构时会阻塞等待任务完成,因此需要确保不会发生阻塞。在这个例子中,我们只是创建了std::future
对象而没有使用它,所以任务在后台异步执行。但是,这种方法可能会导致过多的线程创建,因此对于大量并发的回调,使用线程池可能是更好的选择。
请注意,多线程环境中的观察者模式需要确保观察者和被观察者的状态同步,以避免竞争条件和数据不一致。在这种情况下,可以使用std::mutex
,std::unique_lock
以及其他同步工具。
消息队列
可以使用消息队列来实现在观察者线程中执行回调。消息队列是一种数据结构,它允许线程安全地交换信息。在这种情况下,被观察者将通知作为消息发送到消息队列,然后观察者线程从消息队列中读取消息并执行回调。
这里有一个简化的示例,使用C++的std::queue
、std::mutex
和std::condition_variable
实现了一个简单的消息队列:
#include <iostream> #include <vector> #include <queue> #include <thread> #include <mutex> #include <condition_variable> class MessageQueue { public: void push(const std::function<void()>& msg); std::function<void()> pop(); private: std::queue<std::function<void()>> queue_; std::mutex mutex_; std::condition_variable cv_; }; void MessageQueue::push(const std::function<void()>& msg) { std::unique_lock<std::mutex> lock(mutex_); queue_.push(msg); cv_.notify_one(); } std::function<void()> MessageQueue::pop() { std::unique_lock<std::mutex> lock(mutex_); cv_.wait(lock, [this] { return !queue_.empty(); }); auto msg = queue_.front(); queue_.pop(); return msg; } class Subject; class Observer { public: Observer(Subject* subject); virtual ~Observer() = default; virtual void update() = 0; protected: Subject* subject; }; class Subject { public: void addObserver(Observer* observer); void removeObserver(Observer* observer); void notifyObservers(); protected: std::vector<Observer*> observers; }; Observer::Observer(Subject* subject) : subject(subject) { subject->addObserver(this); } void Subject::addObserver(Observer* observer) { observers.push_back(observer); } void Subject::removeObserver(Observer* observer) { observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end()); } void Subject::notifyObservers() { for (auto& observer : observers) { observer->update(); } } class ConcreteObserver : public Observer { public: ConcreteObserver(Subject* subject, MessageQueue& messageQueue); void update() override; void processMessages(); private: MessageQueue& messageQueue_; std::thread messageProcessorThread_; }; ConcreteObserver::ConcreteObserver(Subject* subject, MessageQueue& messageQueue) : Observer(subject), messageQueue_(messageQueue) { messageProcessorThread_ = std::thread(&ConcreteObserver::processMessages, this); } void ConcreteObserver::update() { messageQueue_.push([this] { std::cout << "Callback executed in observer's thread." << std::endl; }); } void ConcreteObserver::processMessages() { while (true) { auto msg = messageQueue_.pop(); msg(); } } class ConcreteSubject : public Subject { public: void someEvent() { std::cout << "An event occurred in the subject." << std::endl; notifyObservers(); } }; int main() { ConcreteSubject subject; MessageQueue messageQueue; ConcreteObserver observer1(&subject, messageQueue), observer2(&subject, messageQueue); subject.someEvent(); std::this_thread::sleep_for(std::chrono::seconds(1)); return 0; }
在这个示例中,MessageQueue
类提供了一个线程安全的消息队列。观察者的update
方法将回调函数作为消息推送到消息队列,而processMessages
方法从消息队列中读取消息并执行回调。ConcreteObserver
类在构造函数中启动了一个后台线程来执行processMessages
方法,从而确保回调在观察者线程中执行。
注意,在多线程环境中,您可能需要处理线程同步和状态同步等问题。在这个例子中,我们没有处理观察者和被观察者的析构问题,所以当main
函数结束时,messageProcessorThread_
仍在运行。您可能需要在ConcreteObserver
类的析构函数中设置适当的退出条件,以便在不再需要时停止消息处理线程。这可以通过在消息队列中添加一个特殊的“退出”消息来实现。
总之,您可以使用多种方法在观察者线程中执行回调。这里我们讨论了以下方法:
- 使用
std::thread
。 - 使用线程池。
- 使用
std::async
和std::future
。 - 使用消息队列。
根据您的应用程序需求和性能要求,可以选择合适的方法。在多线程环境中,要注意线程同步、状态同步和资源管理等问题,以确保程序的正确性和稳定性。
11.qt信号槽与观察者模式
Qt框架中的信号槽机制允许在不同对象之间进行通信。它类似于观察者模式,但实现方式略有不同。在Qt中,信号(类似于被观察者)和槽(类似于观察者)之间的连接可以是多种类型,其中包括Qt::DirectConnection
(直接连接)和Qt::QueuedConnection
(队列连接)。
- Qt::DirectConnection
当使用Qt::DirectConnection
连接信号和槽时,槽函数将在发送信号的线程中直接执行,类似于观察者模式中在被观察者线程执行回调。 - Qt::QueuedConnection
Qt::QueuedConnection
允许在发送信号的线程与接收信号的线程不同时,在接收信号的线程中执行槽函数。它通过在发送信号的线程中将信号和参数加入到事件队列,然后在接收信号的线程中处理事件队列来实现。
在Qt中,使用队列连接可以实现在观察者线程(槽所属对象的线程)中执行槽函数。Qt内部实现了一个事件循环和事件队列,用于处理不同线程之间的通信。
当使用Qt::QueuedConnection
时,信号发送者线程将信号和参数打包为事件,并将其添加到接收者线程的事件队列中。接收者线程的事件循环在适当的时候处理事件队列,从而在接收者线程中执行槽函数。这种机制类似于之前讨论过的消息队列方法。
要实现Qt::QueuedConnection
,您需要在连接信号和槽时指定连接类型,例如:
connect(sender, SIGNAL(signal()), receiver, SLOT(slot()), Qt::QueuedConnection);
- 通过使用
Qt::QueuedConnection
连接类型,您可以确保槽函数在接收信号的对象所在的线程中执行,实现观察者线程中的回调。
Qt框架中的信号槽机制在使用Qt::QueuedConnection
时是通过内部的消息队列(事件队列)实现的。当信号发送者线程将信号和参数打包为事件并添加到接收者线程的事件队列中时,接收者线程的事件循环处理事件队列并在适当的时候执行槽函数。这使得槽函数能够在接收信号的对象所在的线程(观察者线程)中执行。
总之,Qt信号槽机制在使用Qt::QueuedConnection
时,采用了类似于消息队列的方法来实现在观察者线程中执行槽函数。这种方法允许跨线程安全地传递信号,并在目标线程中执行相关操作。
12. 观察者高级编程
如何做到一对多,但是发送不同的接口?
现在有一个场景,观察者A和观察者B都需要被通知,但是A需要一些数据,但是B不需要.
这时候应该怎么做呢
方式一:定义两个基类接口,一个带数据,一个不带数据.
如果您希望在发送通知时根据观察者的类型调用不同的接口,可以使用C++中的dynamic_cast
实现运行时类型检查。根据观察者的类型,您可以定义不同的接口并在观察者中实现它们。
例如,您可以定义以下两个接口:
class DataObserverInterface { public: virtual void updateWithData(Observable* observable, const Data& data) = 0; }; class NoDataObserverInterface { public: virtual void updateWithoutData(Observable* observable) = 0; };
然后,让需要数据的观察者实现DataObserverInterface
,不需要数据的观察者实现NoDataObserverInterface
。
在Observable
类的notifyObservers
方法中,您可以使用dynamic_cast
检查观察者的类型并根据类型调用不同的接口:
void notifyObservers() { for (const auto& observer : observers_) { if (auto dataObserver = dynamic_cast<DataObserverInterface*>(observer.get())) { Data data; // 准备要发送的数据 dataObserver->updateWithData(this, data); } else if (auto noDataObserver = dynamic_cast<NoDataObserverInterface*>(observer.get())) { noDataObserver->updateWithoutData(this); } else { // 对于未知类型的观察者,可以选择抛出异常、输出警告或忽略 } } }
请注意,使用dynamic_cast
会引入运行时开销,因此在性能敏感的场景中应谨慎使用。在这个例子中,由于dynamic_cast
仅在notifyObservers
方法中使用,其性能影响可能是可以接受的。
另一个值得注意的点是,在C++中使用观察者模式时,建议使用std::weak_ptr
而不是std::shared_ptr
来存储观察者。这可以避免循环引用导致的内存泄漏问题。在调用观察者的方法时,可以通过std::weak_ptr::lock()
方法将其升级为std::shared_ptr
,以确保观察者在调用期间仍然存在。
方式二:使用模版,实例化不同的基类
使得用户可以根据需要选择实例化有参数和无参数的部分。这样可以在编译时处理不同类型的观察者,减少运行时类型检查的需要,同时保持代码简洁。
例如,可以定义一个泛型的观察者接口:
template<typename T> class Observer { public: virtual void update(Observable* observable, const T& data) = 0; };
然后,用户可以选择实例化有参数版本:
class DataObserver : public Observer<Data> { public: void update(Observable* observable, const Data& data) override { // 处理数据的逻辑 } };
或者实例化无参数版本:
class NoDataObserver : public Observer<void> { public: void update(Observable* observable, const void&) override { // 不处理数据的逻辑 } };
这种方式的一个潜在问题是,Observable
类需要处理不同类型的观察者。这可以通过使用类似 std::variant
或 std::any
的方法来解决,或者使用基于多态的方法,例如使用基类指针(如 ObserverBase*
)来存储观察者,然后在派生类中使用模板。
这种方法允许用户在编译时根据需要实例化有参数和无参数的观察者,从而避免了运行时类型检查的开销。当然,具体的实现细节可能会因项目的实际需求而有所不同。
方式三:使用访问者模式
使用访问者模式可以将处理不同类型观察者的逻辑解耦,提高代码可读性和可维护性。以下是使用访问者模式解决这个问题的思路:
- 首先,定义一个
ObserverVisitor
接口,包含多个用于处理不同类型观察者的方法:
class ObserverVisitor { public: virtual void visitDataObserver(DataObserver* observer, Observable* observable, const Data& data) = 0; virtual void visitNoDataObserver(NoDataObserver* observer, Observable* observable) = 0; };
- 接下来,为每种类型的观察者实现
accept
方法,接受一个ObserverVisitor
对象:
class DataObserver : public Observer { public: void accept(ObserverVisitor& visitor, Observable* observable, const Data& data) { visitor.visitDataObserver(this, observable, data); } }; class NoDataObserver : public Observer { public: void accept(ObserverVisitor& visitor, Observable* observable) { visitor.visitNoDataObserver(this, observable); } };
- 在
Observable
类的notifyObservers
方法中,您无需使用dynamic_cast
检查观察者的类型,而是直接调用观察者的accept
方法,并传递访问者对象:
class UpdateObserverVisitor : public ObserverVisitor { public: void visitDataObserver(DataObserver* observer, Observable* observable, const Data& data) override { observer->updateWithData(observable, data); } void visitNoDataObserver(NoDataObserver* observer, Observable* observable) override { observer->updateWithoutData(observable); } }; void Observable::notifyObservers() { UpdateObserverVisitor visitor; Data data; // 准备要发送的数据 for (const auto& observer : observers_) { observer->accept(visitor, this, data); } }
通过这种方式,处理不同类型观察者的逻辑被移到了访问者类中,从而使Observable
类的代码更加简洁。这也让添加新类型的观察者变得更容易,因为您只需在ObserverVisitor
接口中添加相应的处理方法,而无需修改Observable
类。
请注意,访问者模式的一个潜在缺点是它可能导致更多的类和接口,从而增加了代码的复杂性。在使用访问者模式时,请权衡其优缺点,并根据项目的实际需求做出适当的决策。
方式四:依赖注入
使用依赖注入解决这种情况的思路是将处理数据的逻辑抽象成一个接口,然后为不同的观察者提供不同的实现。观察者类依赖于该接口,而不是依赖于具体的实现。通过注入不同的实现,您可以灵活地调整观察者的行为。
以下是一个简化的示例:
- 定义一个处理数据的接口
DataHandler
:
class DataHandler { public: virtual void handleData(Observable* observable, const Data& data) = 0; };
- 为需要数据和不需要数据的观察者分别实现
DataHandler
接口:
class DataObserverHandler : public DataHandler { public: void handleData(Observable* observable, const Data& data) override { // 实现获取并处理数据的逻辑 } }; class NoDataObserverHandler : public DataHandler { public: void handleData(Observable* observable, const Data& data) override { // 实现不需要数据的更新逻辑,忽略data参数 } };
- 在观察者类中,通过构造函数或 setter 方法注入
DataHandler
接口的实现:
class Observer { public: Observer(std::shared_ptr<DataHandler> dataHandler) : dataHandler_(dataHandler) {} void setDataHandler(std::shared_ptr<DataHandler> dataHandler) { dataHandler_ = dataHandler; } void update(Observable* observable, const Data& data) { dataHandler_->handleData(observable, data); } private: std::shared_ptr<DataHandler> dataHandler_; };
- 在创建观察者对象时,注入不同的
DataHandler
实现:
std::shared_ptr<DataHandler> dataObserverHandler = std::make_shared<DataObserverHandler>(); std::shared_ptr<DataHandler> noDataObserverHandler = std::make_shared<NoDataObserverHandler>(); std::shared_ptr<Observer> dataObserver = std::make_shared<Observer>(dataObserverHandler); std::shared_ptr<Observer> noDataObserver = std::make_shared<Observer>(noDataObserverHandler);
这种方法的优点是观察者类变得更简单,它们仅依赖于DataHandler
接口,而不是具体的实现。通过注入不同的实现,您可以灵活地调整观察者的行为,而无需修改观察者类本身。这有助于降低代码的复杂性,提高可维护性。
13. 总结与展望(Conclusion and Future Perspectives)
观察者模式是一种非常实用且经典的设计模式,它在许多场景下都能提供优雅的解决方案。通过使用观察者模式,我们可以实现松散耦合的对象之间的通信,从而提高代码的可维护性和可扩展性。
然而,观察者模式并非适用于所有情况。在考虑使用观察者模式时,应权衡其优缺点,并根据具体需求选择合适的设计模式。在某些情况下,可能需要对观察者模式进行扩展以满足特定需求,例如使用带通知功能的观察者模式或双向观察者模式。
此外,随着C++标准的发展,新的特性(如智能指针、Lambda表达式等)为实现观察者模式提供了新的方法。这些特性可以使观察者模式的实现变得更加简洁和高效。未来,我们可以期待更多的C++特性和工具将进一步简化观察者模式的实现,以及其他设计模式。
最后,随着软件开发领域不断发展,观察者模式和其他设计模式将在各种应用领域发挥重要作用。通过了解和掌握观察者模式,我们可以为构建更为高效、灵活和可维护的软件系统奠定基础。