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

一、观察者模式概述

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

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

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

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

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

二、观察者模式的角色

(一)抽象主题

抽象主题(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. 缺乏变化细节:观察者模式只通知观察者目标对象发生了变化,但没有提供相应的机制让观察者知道目标对象是怎么发生变化的。这在一些需要详细了解变化过程的场景下可能会带来不便。比如在一个数据分析系统中,观察者可能只知道数据发生了变化,但不知道具体是哪些数据发生了变化以及变化的原因。
相关推荐
捕鲸叉3 小时前
C++软件设计模式之外观(Facade)模式
c++·设计模式·外观模式
小小小妮子~4 小时前
框架专题:设计模式
设计模式·框架
先睡4 小时前
MySQL的架构设计和设计模式
数据库·mysql·设计模式
Damon_X12 小时前
桥接模式(Bridge Pattern)
设计模式·桥接模式
越甲八千16 小时前
重温设计模式--享元模式
设计模式·享元模式
码农爱java17 小时前
设计模式--抽象工厂模式【创建型模式】
java·设计模式·面试·抽象工厂模式·原理·23种设计模式·java 设计模式
越甲八千18 小时前
重温设计模式--中介者模式
windows·设计模式·中介者模式
犬余18 小时前
设计模式之桥接模式:抽象与实现之间的分离艺术
笔记·学习·设计模式·桥接模式
Theodore_102219 小时前
1 软件工程——概述
java·开发语言·算法·设计模式·java-ee·软件工程·个人开发
越甲八千21 小时前
重拾设计模式--组合模式
设计模式·组合模式