设计模式之——观察者模式

一、观察者模式概述

观察者模式是一种对象行为模式,它在软件设计中有着广泛的应用。这种模式定义了一种一对多的依赖关系,其中一个主题对象可被多个观察者对象同时监听。当主题对象的状态发生变化时,它会主动发出通知,使得所有依赖于它的观察者对象都能得到通知并被自动更新。

例如,在图形用户界面编程中,一个数据模型可以作为主题,而多个界面组件可以作为观察者。当数据模型中的数据发生变化时,它会通知所有注册的观察者,这些观察者可以是文本框、下拉菜单、图表等界面组件。这些组件会根据数据的变化自动更新自己的显示内容。

在业务对象之间的交互中,观察者模式也非常有用。比如,一个订单系统中,订单状态的变化可以作为主题,而物流系统、库存系统、客户通知系统等可以作为观察者。当订单状态发生变化时,比如从 "已下单" 变为 "已发货",这些观察者系统会收到通知并进行相应的处理,如更新物流信息、减少库存数量、发送通知给客户等。

观察者模式不仅被广泛应用于软件界面元素之间的交互,在权限管理等方面也有广泛的应用。例如,当用户的权限发生变化时,系统中的各个模块可以作为观察者,根据权限的变化进行相应的调整。

总的来说,观察者模式通过定义一种一对多的依赖关系,实现了主题对象和观察者对象之间的解耦,使得系统更加灵活和可维护。

二、观察者模式的角色

(一)抽象主题

抽象主题(Subject)是被观察的对象的抽象表示,它定义了一系列用于管理观察者的方法。通常,抽象主题会维护一个观察者列表,以便在状态发生变化时通知所有注册的观察者。

例如,在一个新闻发布系统中,抽象主题可以是新闻源,它定义了添加、删除记者(观察者)的方法,以及通知记者有新新闻发布的方法。抽象主题的主要作用是提供一个统一的接口,使得具体主题可以方便地管理观察者,而不需要关心具体的观察者类型。

(二)具体主题

具体主题(ConcreteSubject)是抽象主题的具体实现,它代表实际被观察的对象。具体主题通常会在内部状态发生变化时,调用通知方法通知所有注册的观察者。

以电商平台为例,具体主题可以是商品库存系统。当商品库存数量发生变化时,库存系统会通知所有关注该商品的商家(观察者),以便商家及时调整销售策略。具体主题需要实现抽象主题中定义的方法,并负责维护自身的状态和观察者列表。

(三)抽象观察者

抽象观察者(Observer)定义了观察者的通用接口,即响应通知的更新方法。这个方法会在具体主题状态发生变化时被调用,以便观察者可以根据变化做出相应的反应。

比如在天气预报系统中,抽象观察者可以是各种气象设备,它们都需要实现一个更新方法,以便在接收到天气变化的通知时进行相应的处理,如调整显示数据、发送警报等。

(四)具体观察者

具体观察者(ConcreteObserver)是抽象观察者的具体实现,它代表实际的观察者对象。具体观察者会在接收到具体主题的通知时,自动做出特定的响应。

以股市行情系统为例,具体观察者可以是不同的投资者。当股票价格发生变化(具体主题状态变化)时,投资者会根据自己的投资策略做出相应的操作,如买入、卖出或持有股票。具体观察者需要实现抽象观察者中定义的更新方法,并根据具体的业务需求进行相应的处理。

三、观察者模式的实现方式

(一)创建抽象主题角色

在 C++ 中,可以定义一个抽象主题类Subject,它包含一个观察者列表和一系列管理观察者的方法。以下是一个简单的实现:

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;

// 定义抽象基类 Subject,表示被观察的主题
class Subject {
public:
    // 纯虚函数,用于向主题添加一个观察者
    // 参数 observer 是指向 Observer 类型对象的指针,表示要添加的观察者
    // 由于该函数没有函数体且被声明为纯虚函数,所以任何继承自 Subject 的具体类都必须实现该函数
    virtual void addObserver(Observer* observer) = 0;

    // 纯虚函数,用于从主题中移除一个观察者
    // 参数 observer 同样是指向 Observer 类型对象的指针,表示要移除的观察者
    // 继承自 Subject 的具体类必须实现此函数,以提供移除观察者的具体逻辑
    virtual void removeObserver(Observer* observer) = 0;

    // 纯虚函数,用于通知所有已注册的观察者
    // 当主题状态发生变化或有事件发生时,调用此函数来通知观察者进行相应处理
    // 具体的通知方式和时机由继承自 Subject 的具体类来决定,因此该函数也是纯虚函数,需要在派生类中实现
    virtual void notifyObservers() = 0;

private:
    // 定义一个私有成员变量 observers,用于存储所有已注册的观察者对象指针
    // 这里使用 std::vector 容器来管理观察者列表,方便添加、删除和遍历操作
    vector<Observer*> observers;
};

这个抽象主题类定义了添加观察者、删除观察者和通知观察者的纯虚函数,具体的实现将在具体主题类中完成。

(二)创建具体主题角色

具体主题类ConcreteSubject继承自抽象主题类Subject,实现了具体的业务逻辑和通知观察者的方法。例如:

cpp 复制代码
// 定义抽象基类 Observer,它将由具体的观察者类继承并实现其抽象方法
class Observer 
{
public:
    // 纯虚函数,用于定义当被观察主题状态改变时观察者的更新行为
    // 具体的更新逻辑将在派生类中实现
    virtual void update() = 0;
};

// 定义具体主题类 ConcreteSubject,它继承自抽象基类 Subject
class ConcreteSubject : public Subject 
{
public:
    // 实现抽象基类中的添加观察者方法
    // 将传入的观察者指针添加到观察者列表中
    void addObserver(Observer* observer) override 
    {
        observers.push_back(observer);
    }

    // 实现抽象基类中的移除观察者方法
    // 遍历观察者列表,找到与传入指针相等的观察者并将其从列表中删除
    void removeObserver(Observer* observer) override 
    {
        for (auto it = observers.begin(); it!= observers.end(); ++it) 
        {
            if (*it == observer) 
            {
                observers.erase(it);
                break;
            }
        }
    }

    // 实现抽象基类中的通知观察者方法
    // 遍历观察者列表,调用每个观察者的 update 方法,通知它们主题状态已改变
    void notifyObservers() override 
    {
        for (auto observer : observers) 
        {
            observer->update();
        }
    }

    // 设置主题的数据,并在数据更新后通知所有观察者
    void setData(int newData) 
    {
        data = newData;
        notifyObservers();
    }

private:
    // 存储主题相关的数据
    int data;

    // 存储所有注册到该主题的观察者指针
    vector<Observer*> observers;
};

在这个具体主题类中,实现了添加、删除观察者和通知观察者的方法。当数据发生变化时,通过调用setData方法,会通知所有注册的观察者。

(三)创建抽象观察者

抽象观察者类Observer定义了一个更新方法,具体观察者将实现这个方法来响应主题的状态变化。

cpp 复制代码
// 定义抽象基类 Observer,用于表示观察者
class Observer 
{
public:
    // 纯虚函数 update,用于定义当被观察的主题状态发生变化时,观察者需要执行的更新操作
    // 具体的更新逻辑将由继承自 Observer 的具体观察者类来实现
    // 由于该函数是纯虚函数,所以 Observer 类不能被实例化,只能作为基类被继承
    virtual void update() = 0;
};

(四)创建具体观察者

具体观察者类ConcreteObserver实现了抽象观察者类的更新方法。例如:

cpp 复制代码
// 定义具体观察者类 ConcreteObserver,继承自抽象基类 Observer
class ConcreteObserver : public Observer 
{
public:
    // 构造函数,用于初始化观察者对象的名称
    // 参数 name 是观察者的名称,将其存储在成员变量 observerName 中
    ConcreteObserver(const std::string& name) : observerName(name) {}

    // 实现抽象基类 Observer 中的纯虚函数 update
    // 当被观察的主题状态发生变化时,该函数会被主题调用,用于执行观察者的特定更新操作
    // 在这个例子中,它只是简单地输出一条消息,表明该观察者接收到了更新,并显示观察者的名称
    void update() override {
        std::cout << "Observer " << observerName << " received update." << std::endl;
    }

private:
    // 存储观察者的名称
    string observerName;
};

(五)测试类验证

以下是一个测试类来验证观察者模式的实现:

cpp 复制代码
int main() 
{
    // 创建一个具体主题对象 subject
    ConcreteSubject subject;
    // 创建两个具体观察者对象 observer1 和 observer2,并分别传入不同的名称进行初始化
    ConcreteObserver observer1("Observer 1");
    ConcreteObserver observer2("Observer 2");
    // 将 observer1 和 observer2 添加到 subject 的观察者列表中
    subject.addObserver(&observer1);
    subject.addObserver(&observer2);
    // 调用 subject 的 setData 方法,设置主题的数据,并触发通知观察者的操作
    subject.setData(10);
    return 0;
}

在业务中,比如在一个问答系统中,可以使用 JDK 封装的方法实现观察者模式。假设问题是抽象主题,回答者是观察者。当问题状态发生变化(如被编辑、有新的回答等)时,通知所有关注这个问题的回答者。具体实现可以类似如下:

1.定义问题类(抽象主题):

cpp 复制代码
class Question {
public:
    virtual void addAnswerer(Answerer* answerer) = 0;
    virtual void removeAnswerer(Answerer* answerer) = 0;
    virtual void notifyAnswerers() = 0;
private:
    std::vector<Answerer*> answerers;
};

2.具体问题类:

cpp 复制代码
class ConcreteQuestion : public Question {
public:
    void addAnswerer(Answerer* answerer) override {
        answerers.push_back(answerer);
    }
    void removeAnswerer(Answerer* answerer) override {
        for (auto it = answerers.begin(); it!= answerers.end(); ++it) {
            if (*it == answerer) {
                answerers.erase(it);
                break;
            }
        }
    }
    void notifyAnswerers() override {
        for (auto answerer : answerers) {
            answerer->update();
        }
    }
    void editQuestion() {
        // 问题被编辑,通知回答者
        notifyAnswerers();
    }
};

3.定义回答者类(抽象观察者):

cpp 复制代码
class Answerer {
public:
    virtual void update() = 0;
};

4.具体回答者类:

cpp 复制代码
class ConcreteAnswerer : public Answerer {
public:
    ConcreteAnswerer(const std::string& name) : answererName(name) {}
    void update() override {
        std::cout << "Answerer " << answererName << " received update about question change." << std::endl;
    }
private:
    std::string answererName;
};

4.测试:

cpp 复制代码
int main() {
    ConcreteQuestion question;
    ConcreteAnswerer answerer1("Answerer 1");
    ConcreteAnswerer answerer2("Answerer 2");
    question.addAnswerer(&answerer1);
    question.addAnswerer(&answerer2);
    question.editQuestion();
    return 0;
}

四、观察者模式的应用场景

(一)事件驱动编程

在事件驱动编程中,观察者模式是基础。许多框架和库都采用了这种模式,例如 Java Swing 和.NET WinForms 中的事件处理机制。当一个特定的事件发生时,如按钮被点击、鼠标移动等,相关的观察者会收到通知并执行相应的操作。这样可以实现代码的解耦,使得不同的功能模块可以独立开发和维护。

(二)界面组件与数据绑定

在 GUI 编程中,界面组件通常需要与数据源进行绑定和同步更新。观察者模式可以很好地解决这种问题。以一个天气应用为例,当天气数据发生变化时,作为主题的天气数据源会通知作为观察者的界面组件,如温度显示标签、天气图标等,使其自动更新显示。这样可以确保用户界面始终显示最新的天气信息。

(三)消息订阅与发布

在分布式系统或消息队列中,消息的订阅和发布可以使用观察者模式来实现解耦。例如,在一个微服务架构中,不同的服务可以作为观察者订阅特定的消息主题。当有新的消息发布到该主题时,所有订阅的服务都会收到通知并进行相应的处理。这样可以实现服务之间的异步通信和松耦合。

(四)属性绑定

在一些富客户端应用程序中,属性之间的绑定可以使用观察者模式来实现。例如 JavaFX 中的属性绑定机制。当一个属性的值发生变化时,与之绑定的其他属性会自动更新。这样可以简化代码,提高开发效率。

(五)状态管理

在复杂的状态管理场景中,观察者模式可以帮助管理状态变化的订阅和通知。例如 Redux 和 Vuex 等状态管理库。当应用的状态发生变化时,相关的组件会收到通知并更新自己的显示。这样可以确保用户界面始终与应用的状态保持一致。

(六)触发器和回调

在系统中某个事件发生时,需要触发相应的回调函数。观察者模式可以方便地实现这种机制。例如,在一个文件上传系统中,当文件上传完成时,可以触发一个回调函数通知用户上传结果。这样可以提高系统的响应性和用户体验。

五、观察者模式的优缺点

(一)优点

  1. 解耦性良好:观察者模式将观察者和被观察者分离开来,两者之间仅通过抽象的接口进行交互。这使得它们之间的依赖关系变得松散,一个对象的变化不会直接影响到另一个对象,提高了系统的可维护性和可扩展性。例如在一个电商系统中,商品的库存变化是被观察者,而关注该商品的消费者可以作为观察者。当库存发生变化时,系统只需通知消费者,而无需关心消费者具体如何处理这个变化。
  2. 灵活性高:观察者模式允许在运行时动态地添加和删除观察者,使得系统能够灵活地适应不同的需求。比如在一个新闻发布系统中,新的用户可以随时订阅新闻,而已经订阅的用户也可以随时取消订阅,系统无需进行大规模的修改。
  3. 建立触发机制:观察者模式提供了一种有效的触发机制,当被观察者的状态发生变化时,能够自动通知所有的观察者。这种机制可以确保相关的对象及时得到更新,提高了系统的响应性。例如在一个股票交易系统中,当股票价格发生变化时,所有关注该股票的投资者都能及时收到通知。

(二)缺点

  1. 通知耗时:如果一个被观察者对象有很多直接和间接的观察者,那么在被观察者状态发生变化时,通知所有观察者会花费很多时间。以一个大型社交网络平台为例,当一个热门话题出现变化时,可能会有大量的用户作为观察者需要被通知,这可能会导致系统性能下降。
  2. 循环依赖风险:如果观察者和观察目标之间存在循环依赖关系,那么可能会导致系统崩溃。例如,在一个复杂的软件系统中,如果两个模块相互观察对方的状态变化,并且在某些情况下形成了循环调用,就可能引发严重的问题。
  3. 缺乏变化细节:观察者模式只通知观察者目标对象发生了变化,但没有提供相应的机制让观察者知道目标对象是怎么发生变化的。这在一些需要详细了解变化过程的场景下可能会带来不便。比如在一个数据分析系统中,观察者可能只知道数据发生了变化,但不知道具体是哪些数据发生了变化以及变化的原因。
相关推荐
杨充19 小时前
10.可测试性实战设计
设计模式·开源·代码规范
杨充19 小时前
9.重构十二式的实战
设计模式·开源·代码规范
杨充19 小时前
6.设计原则的全景图
设计模式·开源·全栈
杨充19 小时前
2.面向对象的特性
设计模式
杨充19 小时前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充19 小时前
8.反模式与坏味道
设计模式·开源·代码规范
杨充19 小时前
3.接口vs抽象类比较
设计模式
咖啡八杯1 天前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法
咖啡八杯1 天前
GoF设计模式——解释器模式
java·后端·spring·设计模式