设计模式(5)——观察者模式

文章目录

  • 五、day5
  • [1. 什么是观察者模式](#1. 什么是观察者模式)
  • [2. 观察者模式的结构](#2. 观察者模式的结构)
  • [3. 观察者模式的应用场景](#3. 观察者模式的应用场景)
  • [4. 观察者模式优缺点](#4. 观察者模式优缺点)
  • [5. 观察者模式的实现](#5. 观察者模式的实现)

五、day5

观察者设计模式

观察者模式 - 摩根斯 | 爱编程的大丙

深入理解设计模式(八):观察者模式 - 一指流砂~ - 博客园

1. 什么是观察者模式

观察者模式用于解决一个对象的状态变化需要通知多个对象的问题。核心思想是:

  1. 有一个主题对象(被观察者),负责维护数据和状态。
  2. 有多个观察者对象,想了解主题对象的变化。
  3. 当主题对象状态发生变化时,会通知所有观察者。

这样设计的优点是低耦合 ,主题和观察者之间的依赖关系很松散,方便扩展和维护。简单来说:主题负责变化,观察者负责监听,变化后自动通知监听者。


图片来源:https://refactoringguru.cn/design-patterns/observer

2. 观察者模式的结构

举例说明:

问题来源

假如你有两种类型的对象: 顾客商店 。 顾客对某个特定品牌的产品非常感兴趣 (例如最新型号的 iPhone 手机), 而该产品很快将会在商店里出售。顾客可以每天来商店看看产品是否到货。 但如果商品尚未到货时, 绝大多数来到商店的顾客都会空手而归。

另一方面, 每次新产品到货时, 商店可以向所有顾客发送邮件 (可能会被视为垃圾邮件)。 这样, 部分顾客就无需反复前往商店了, 但也可能会惹恼对新产品没有兴趣的其他顾客。

我们似乎遇到了一个矛盾: 要么让顾客浪费时间检查产品是否到货, 要么让商店浪费资源去通知没有需求的顾客

解决方法:

拥有一些值得关注的状态的对象通常被称为目标 , 由于它要将自身的状态改变通知给其他对象, 我们也将其称为发布者 (publisher)。 所有希望关注发布者状态变化的其他对象被称为订阅者 (subscribers)。

观察者模式建议你为发布者类添加订阅机制, 让每个对象都能订阅或取消订阅发布者事件流。 不要害怕! 这并不像听上去那么复杂。 实际上, 该机制包括 1) 一个用于存储订阅者对象引用的列表成员变量; 2) 几个用于添加或删除该列表中订阅者的公有方法。


订阅机制允许对象订阅事件通知 图片来源:https://www.cnblogs.com/xuwendong/p/9814417.html#_label0

现在, 无论何时发生了重要的发布者事件, 它都要遍历订阅者并调用其对象的特定通知方法。

实际应用中可能会有十几个不同的订阅者类跟踪着同一个发布者类的事件, 你不会希望发布者与所有这些类相耦合的。 此外如果他人会使用发布者类, 那么你甚至可能会对其中的一些类一无所知。

因此, 所有订阅者都必须实现同样的接口, 发布者仅通过该接口与订阅者交互。 接口中必须声明通知方法及其参数, 这样发布者在发出通知时还能传递一些上下文数据。

如果你的应用中有多个不同类型的发布者, 且希望订阅者可兼容所有发布者, 那么你甚至可以进一步让所有发布者遵循同样的接口。 该接口仅需描述几个订阅方法即可。 这样订阅者就能在不与具体发布者类耦合的情况下通过接口观察发布者的状态。

因此,观察者模式的结构可总结如下:


图片来源:https://www.cnblogs.com/xuwendong/p/9814417.html#_label0

观察者模式的结构中包含四种角色:

(1)主题(Subject):主题是一个接口,该接口规定了具体主题需要实现的方法。这些方法通常包括添加观察者、移除观察者,以及在数据发生变化时通知所有观察者的方法。主题的职责是维护一个观察者的列表,并确保数据变化时将通知传递给所有注册的观察者。

(2)观察者(Observer):观察者是一个接口,用于定义具体观察者需要实现的方法,通常是更新数据的方法。通过实现该接口,观察者能够接收主题的通知,并根据通知内容更新自身状态。观察者关注主题的状态变化,并对这些变化作出响应。

(3)具体主题(ConcreteSubject) :具体主题是实现了主题接口的类实例,它包含可能会频繁变化的数据。这些数据的变化会触发通知机制,通知所有注册的观察者。具体主题通常会使用一个集合(如 ArrayList)来存储所有观察者的引用。通过这一集合,具体主题能够高效地通知所有观察者,使其更新数据。

(4)具体观察者(ConcreteObserver):具体观察者是实现了观察者接口的类实例。具体观察者通常包含一个主题接口的引用,以便能够向具体主题注册自己为观察者或从主题中注销自己。这样,具体观察者可以选择是否接收主题的通知。当主题发生变化时,具体观察者会根据通知更新自身状态或执行相应操作。

3. 观察者模式的应用场景

观察者模式主要应用于以下两个方面:

(1)当一个对象的数据更新时需要通知其他对象,但这个对象又不希望和被通知的那些对象形成紧耦合。

股票交易系统中,当股票的价格发生变化时,系统需要通知所有关注该股票的投资者,但股票交易系统并不知道具体有多少投资者在关注,也不希望与这些投资者建立直接联系。

具体示例:

  • 股票价格是"主题",表示股票的当前价格。
  • 投资者是"观察者",他们对股票价格的变化感兴趣。
  • 当股票价格更新时,系统会自动通知所有关注的投资者,他们可以根据价格变化调整自己的投资决策。

通过观察者模式,股票交易系统只需要维护一个投资者的列表,当价格变化时通知这些投资者即可,避免了与每个投资者形成紧耦合的关系。

(2)当一个对象的数据更新时,这个对象需要让其他对象也各自更新自己的数据,但这个对象不知道具体有多少对象需要更新数据。

天气预报系统需要定时更新天气数据(如温度、湿度、气压),并且这些数据需要显示在多个设备上(如手机、电脑、电视等)。天气系统并不知道具体有多少设备需要显示这些信息,也不希望与设备的具体实现耦合在一起。

具体示例:

  • 天气预报系统是"主题",它负责收集和发布最新的天气数据。
  • 各种显示设备(如手机 App、电脑屏幕上的天气插件、电视上的天气频道)是"观察者",它们需要接收天气系统的通知并更新显示内容。
  • 当天气数据更新时,天气预报系统会通知所有注册的设备,这些设备各自更新显示的天气数据。

通过这种方式,天气系统只需通知设备更新数据,而无需关心每种设备的具体实现,保证了系统的灵活性和扩展性。

观察者模式的两个难点:

异步处理问题

被观察者发生动作了,观察者要做出回应,如果观察者比较多,而且处理时间比较长怎么办?那就用异步,异步处理就要考虑线程安全和队列的问题。如果我们已经学习过网络编程和线程安全的知识,这其实不难,请参考本文之前我写的一些文章。

广播链的问题

数据库的触发器有一个触发器链的问题,比如表 A 上写了一个触发器,内容是一个字段更新后更新表 B 的一条数据,而表 B 上也有个触发器,要更新表 C,表 C 也有触发器...,完蛋了,这个数据库基本上就毁掉了!

我们的观察者模式也是一样的问题,一个观察者可以有双重身份,即使观察者,也是被观察者,这没什么问题,但是链一旦建立,这个逻辑就比较复杂,可维护性非常差,根据经验建议:在一个观察者模式中最多出现一个对象既是观察者也是被观察者(既要更新自己还要通知别人),也就是说消息最多转发一次(传递两次),这还是比较好控制的

4. 观察者模式优缺点

优点:

1、具体主题和具体观察者是松耦合关系。由于主题接口仅仅依赖于观察者接口,因此具体主题只是知道它的观察者是实现观察者接口的某个类的实例,但不需要知道具体是哪个类。同样,由于观察者仅仅依赖于主题接口,因此具体观察者只是知道它依赖的主题是实现主题接口的某个类的实例,但不需要知道具体是哪个类。

2、观察者模式满足"开-闭原则"。主题接口仅仅依赖于观察者接口,这样,就可以让创建具体主题的类也仅仅是依赖于观察者接口,因此,如果增加新的实现观察者接口的类,不必修改创建具体主题的类的代码。同样,创建具体观察者的类仅仅依赖于主题接口,如果增加新的实现主题接口的类,也不必修改创建具体观察者类的代码。

缺点:

1、订阅者的通知顺序是随机的。

2、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

3、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。

5. 观察者模式的实现

仔细检查你的业务逻辑,试着将其拆分为两个部分:独立于其他代码的核心功能将作为主题 ;其他代码则转化为一组观察者

  1. 声明观察者接口

    定义观察者接口,接口中至少应声明一个 update 方法,用于接收来自主题的通知。

  2. 声明主题接口

    定义主题接口,并提供一些方法,用于在观察者列表中添加或删除观察者。记住,主题必须仅通过观察者接口与观察者交互,而不直接依赖具体的观察者实现。

  3. 存储观察者列表并实现订阅方法

    确定观察者列表的存放位置,并实现用于添加和删除观察者的方法。通常,主题的订阅管理逻辑是通用的,因此可以将观察者列表放置在一个扩展自主题接口的抽象类中,由具体主题继承这些订阅行为。

    如果需要在现有的类结构中应用观察者模式,也可以采用组合的方式:将订阅逻辑封装到一个独立的对象中,让所有具体主题通过组合关系使用该对象。

  4. 创建具体主题类

    具体主题是实现主题接口的类,每次具体主题的状态发生变化时,必须通知所有观察者。它还需要存储当前状态,以便在通知观察者时提供相关数据。

  5. 实现具体观察者类

    在具体观察者类中实现 update 方法。大多数观察者需要从主题中获取与事件相关的上下文数据,这些数据可以通过通知方法的参数传递。

    另一种选择是观察者接收到通知后直接从主题中获取所有数据。在这种情况下,主题需要通过 update 方法将自身作为参数传递给观察者。此外,还可以通过构造函数将具体主题与具体观察者永久绑定,但这种方式灵活性较低。

  6. 注册观察者到主题中

    客户端需要实例化所有具体观察者,并在相应的具体主题中完成注册工作,从而建立起主题和观察者之间的依赖关系。

接下来通过代码进行演示:

1、Observer类---抽象观察者,为所有具体观察者定义一个接口,在得到主题通知时更新自己。

这个接口叫做更新接口,抽象观察者一般用一个抽象类或者一个接口实现。更新接口通常包括一个Update方法,这个方法叫做更新方法。

cpp 复制代码
class Observer {
public:
    virtual ~Observer() {} // 虚析构函数,确保子类析构函数被正确调用
    virtual void Update() = 0; // 纯虚函数,表示抽象方法
};

2、Subject类---主题或者抽象通知者,一般用一个抽象类或者一个接口实现。

它把所有对观察者对象的引用保存到一个聚集里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者。

cpp 复制代码
class Observer;
class Subject {
private:
    std::vector<Observer*> observers; // 存储观察者

public:
    // 增加观察者
    void Attach(Observer* observer) {
        observers.push_back(observer);
    }

    // 移除观察者
    void Detach(Observer* observer) {
        observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end());
    }

    // 通知所有观察者
    void Notify() {
        for (Observer* observer : observers) {
            observer->Update();
        }
    }
};

3、ConcreteSubject类---具体主题或者具体通知者,将有关状态存入具体观察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发送通知。

具体主题角色通常用一个具体类实现。

cpp 复制代码
class ConcreteSubject : public Subject {
private:
    std::string subjectState; // 具体被观察者的状态

public:
    // 获取具体被观察者的状态
    std::string GetSubjectState() const {
        return subjectState;
    }

    // 设置具体被观察者的状态
    void SetSubjectState(const std::string& state) {
        subjectState = state;
    }
};

4、ConcreteObserver类---具体观察者,实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。

具体观察者角色可以保存一个指向具体主题对象的引用。具体观察者角色通常用一个具体类实现。

cpp 复制代码
class ConcreteObserver : public Observer {
private:
    std::string name;               // 观察者名称
    std::string observerState;      // 观察者的状态
    ConcreteSubject* subject;       // 观察的具体主题

public:
    // 构造函数
    ConcreteObserver(ConcreteSubject* subject, const std::string& name)
        : subject(subject), name(name) {}

    // 实现更新方法
    void Update() override {
        observerState = subject->GetSubjectState(); // 获取主题的状态
        std::cout << "观察者 " << name << " 的新状态是 " << observerState << std::endl;
    }

    // 获取具体主题
    ConcreteSubject* GetSubject() const {
        return subject;
    }

    // 设置具体主题
    void SetSubject(ConcreteSubject* newSubject) {
        subject = newSubject;
    }
};

5、客户端代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <memory>
#include <string>

// 观察者基类
class Observer {
public:
    virtual ~Observer() = default;
    virtual void Update() = 0;
};

// 主题基类
class Subject {
private:
    std::vector<Observer*> observers; // 存储观察者的集合

public:
    virtual ~Subject() = default;

    // 添加观察者
    void Attach(Observer* observer) {
        observers.push_back(observer);
    }

    // 移除观察者
    void Detach(Observer* observer) {
        observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end());
    }

    // 通知所有观察者
    void Notify() {
        for (Observer* observer : observers) {
            observer->Update();
        }
    }
};

// 具体主题
class ConcreteSubject : public Subject {
private:
    std::string subjectState; // 具体主题的状态

public:
    std::string GetSubjectState() const {
        return subjectState;
    }

    void SetSubjectState(const std::string& state) {
        subjectState = state;
    }
};

// 具体观察者
class ConcreteObserver : public Observer {
private:
    std::string name;               // 观察者名称
    std::string observerState;      // 观察者的状态
    ConcreteSubject* subject;       // 观察的具体主题

public:
    ConcreteObserver(ConcreteSubject* subject, const std::string& name)
        : subject(subject), name(name) {}

    void Update() override {
        observerState = subject->GetSubjectState();
        std::cout << "观察者 " << name << " 的新状态是 " << observerState << std::endl;
    }
};

int main() {
    ConcreteSubject cs;

    // 创建观察者并注册到主题中
    ConcreteObserver observer1(&cs, "X");
    ConcreteObserver observer2(&cs, "Y");
    ConcreteObserver observer3(&cs, "Z");

    cs.Attach(&observer1);
    cs.Attach(&observer2);
    cs.Attach(&observer3);

    // 更新主题状态并通知观察者
    cs.SetSubjectState("ABC");
    cs.Notify();

    return 0;
}

输出为:

yaml 复制代码
观察者 X 的新状态是 ABC
观察者 Y 的新状态是 ABC
观察者 Z 的新状态是 ABC
相关推荐
快乐非自愿35 分钟前
C++中的各种锁
java·c++·算法
sun0077002 小时前
c++开源协程库libgo介绍及使用,srs协程,boost协程 Boost::fiber
c++
旧物有情2 小时前
蓝桥杯历届真题 # 封闭图形个数(C++,Java)
java·c++·蓝桥杯
重生之我在20年代敲代码2 小时前
【C++入门】详解(中)
开发语言·c++·笔记
羑悻的小杀马特3 小时前
【Artificial Intelligence篇】AI 入侵家庭:解锁智能生活的魔法密码,开启居家梦幻新体验
c++·人工智能·生活
花里胡哨的菜只因3 小时前
关于在windows系统中编译ffmpeg并导入到自己项目中这件事
c++·windows·ffmpeg
我想学LINUX3 小时前
【2024年华为OD机试】 (A卷,100分)- 对称美学(Java & JS & Python&C/C++)
java·c语言·javascript·c++·python·华为od
每天敲200行代码3 小时前
Linux开发工具--vim编辑器-gcc/g++编译器-gdb调试器
linux·c++·编辑器·vim·gdb
闻缺陷则喜何志丹3 小时前
【C++图论 BFS】1129. 颜色交替的最短路径|1779
c++·力扣·图论·最短路·宽度优先·颜色·交替