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

一、观察者模式概述

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

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

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

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

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

二、观察者模式的角色

(一)抽象主题

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

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

(二)具体主题

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

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

(三)抽象观察者

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

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

(四)具体观察者

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

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

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

(一)创建抽象主题角色

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

#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,实现了具体的业务逻辑和通知观察者的方法。例如:


// 定义抽象基类 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定义了一个更新方法,具体观察者将实现这个方法来响应主题的状态变化。

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

(四)创建具体观察者

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


// 定义具体观察者类 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;
};

(五)测试类验证

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

int main() 
{// 创建一个具体主题对象 subjectConcreteSubject 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.定义问题类(抽象主题):

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.具体问题类:

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.定义回答者类(抽象观察者):

class Answerer {
public:virtual void update() = 0;
};

4.具体回答者类:

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.测试:

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. 缺乏变化细节:观察者模式只通知观察者目标对象发生了变化,但没有提供相应的机制让观察者知道目标对象是怎么发生变化的。这在一些需要详细了解变化过程的场景下可能会带来不便。比如在一个数据分析系统中,观察者可能只知道数据发生了变化,但不知道具体是哪些数据发生了变化以及变化的原因。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/7412.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

C语言-详细讲解-洛谷P1909 [NOIP2016 普及组] 买铅笔

目录 1.题目要求 2.题目解读 3.代码实现 1.题目要求 2.题目解读 这道题主要考察了顺序结构&#xff0c;需要我们分别计算三种包装的花费并进行比较&#xff0c;需要思考的就是如何计算包装花费。 3.代码实现 #include<stdio.h> int main(){int n,a1,a2,b1,b2,c1,c2…

刷题日记1

手机 题目描述 一般的手机的键盘是这样的&#xff1a; 要按出英文字母就必须要按数字键多下。例如要按出 x \tt x x 就得按 9 9 9 两下&#xff0c;第一下会出 w \tt w w&#xff0c;而第二下会把 w \tt w w 变成 x \tt x x。 0 0 0 键按一下会出一个空格。 你的任务是…

程序员的生活周刊 #7:耐克总裁被裁记

0. 庙宇 这张图来自 Tianshu Liu&#xff0c; 被树木环绕的宝塔庙宇 1. 耐克总裁 耐克最近的总裁 John Donahoe 干了 5 年&#xff0c;终于被裁掉了。 这位总裁即不了解球鞋文化&#xff0c;也没有零售经验&#xff0c;但不懂事的董事会还是聘用它&#xff0c;寄托把耐克从运…

C语言定义字符串数组

一、字符串 在C语言中&#xff0c;字符串数组是一种存储多个字符串的数组。每个字符串本身是一个字符数组&#xff0c;以空字符&#xff08;\0&#xff09;结尾。 二、定义字符串数组 2.1 字符数组组成的数组 这种方式可以存储固定长度的字符串。示例&#xff1a; //存储5…

解决Postman一直在转圈加载无法打开问题的方法

在使用Postman这款强大的API测试工具时&#xff0c;有时可能会遇到程序长时间加载而无法正常使用的情况。面对这样的问题&#xff0c;可以尝试以下几种解决办法&#xff1a; 方法一&#xff1a;直接运行Postman可执行文件 定位到Postman的安装目录 如果您不确定Postman的具体安…

谷歌浏览器安装 Vue.js devtools 插件

文章目录 1. 安装2. 使用3. 注意 1. 安装 ① 搜索极简插件&#xff1a;https://chrome.zzzmh.cn/index ② 搜索框输入 Vue&#xff0c;选择 Vue.js devtools ③ 从历史版本里面选择并下载&#xff0c;选择 6.4 版本的就行 ④ 打开浏览器&#xff0c;右上角三个点 → 扩展程序…

计算机毕业设计Python+图神经网络考研院校推荐系统 考研分数线预测 考研推荐系统 考研爬虫 考研大数据 Hadoop 大数据毕设 机器学习 深度学习

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

微服务系列六:分布式事务与seata

目录 实验环境说明 前言 一、分布式事务问题与策略 1.1 分布式事务介绍 1.2 分布式事务解决策略分析 二、分布式事务解决方案 Seata 2.1 认识Seata 2.2 Seata的工作原理 2.3 部署Seata微服务 2.3.1 准备数据库表 2.3.2 准备配置文件 2.3.3 docker部署 2.4 微服务集…

adb:Android调试桥

Android 调试桥 (adb) 是一种功能多样的命令行工具&#xff0c;可以通过命令行与设备进行通信。 查询设备 adb devices adb 会创建一个字符串&#xff0c;用于通过端口号唯一标识设备。 adb devices -l 加入 -l 选项&#xff0c;devices 命令会告知设备是什么。当连接了多个…

【C语言】分支和循环详解(下)猜数字游戏

与诸君共进步&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 文章目录 1. 随机数的生成2. 猜数字小游戏的实现 1. 随机数的生成 掌握了前⾯学习的这些知识&#xff0c;我们就可以写⼀些稍微有趣的代码了&#xff0c;⽐如&#xff1a; 写⼀个猜数字游戏 游戏要求…

Java | Leetcode Java题解之第546题移除盒子

题目&#xff1a; 题解&#xff1a; class Solution {int[][][] dp;public int removeBoxes(int[] boxes) {int length boxes.length;dp new int[length][length][length];return calculatePoints(boxes, 0, length - 1, 0);}public int calculatePoints(int[] boxes, int l…

Pytorch学习--神经网络--现有网络模型的使用及修改

一、VGG16 weights (Optional[VGG16_Weights]): 这个参数是可选的&#xff0c;指的是预训练的权重。用户可以选择使用不同的预训练权重&#xff0c;具体可参见 VGG16_Weights 的详细说明。默认情况下&#xff0c;如果不提供此参数&#xff0c;模型将不会使用任何预训练权重。 p…

内部知识库:优化企业培训流程的关键驱动力

在当今快速变化的商业环境中&#xff0c;企业培训的重要性日益凸显。内部知识库作为整合、管理和分享企业内部学习资源的关键工具&#xff0c;正逐步成为优化企业培训流程的核心。以下将探讨内部知识库如何通过多种功能&#xff0c;助力企业提升培训效率、质量和员工满意度。 …

若依系统前端项目解读——从使用过程解读

登录系统 用户初次登录&#xff0c;浏览器中未存用户信息&#xff08;token&#xff09;&#xff0c;需向后端请求并保存至浏览器中用户再次登录系统&#xff0c;向后端发请求会携带token在请求头中&#xff0c;并与后端Redis缓存的token比较&#xff0c;判断token是否还在有效…

前后端交互接口(三)

前后端交互接口&#xff08;三&#xff09; 前言 前两集我们先做了前后端交互接口的约定以及浅浅的阅读了一些proto代码。那么这一集我们就来看看一些重要的proto代码&#xff0c;之后把protobuffer给引入我们的项目当中&#xff01; gateway.proto 我们来看一眼我们的网关…

【Python TensorFlow】进阶指南

在前文中&#xff0c;我们介绍了TensorFlow的基础知识及其在实际应用中的初步使用。现在&#xff0c;我们将进一步探讨TensorFlow的高级特性&#xff0c;包括模型优化、评估、选择、高级架构设计、模型部署、性能优化等方面的技术细节&#xff0c;帮助读者达到对TensorFlow的精…

2款使用.NET开发的数据库系统

今天大姚给大家分享2款使用.NET开发且开源的数据库系统。 Garnet Garnet是一款由微软研究院基于.NET开源的高性能、跨平台的分布式缓存存储数据库&#xff0c;该项目提供强大的性能&#xff08;吞吐量和延迟&#xff09;、可扩展性、存储、恢复、集群分片、密钥迁移和复制功能…

【react】React Router基础知识

1. 基础用法 npm i react-router-dom通过浏览器地址栏的切换&#xff0c;可以实现不同组件之间的切换。 import React from "react"; import ReactDOM from "react-dom/client"; // import App from "./App"; import reportWebVitals from &qu…

std::back_inserter

std::back_inserter 是 C 标准库中的一个函数模板&#xff0c;它用于创建一个插入迭代器&#xff08;insert iterator&#xff09;&#xff0c;这个迭代器可以在容器末尾插入新元素。它定义在 <iterator> 头文件中。 函数原型 template <typename Container> bac…

使用 FFmpeg 进行音视频转换的相关命令行参数解释

FFmpeg 是一个强大的多媒体框架&#xff0c;能够解码、编码、转码、录制、播放以及流化几乎所有类型的音频和视频。它广泛应用于音视频处理任务中&#xff0c;包括格式转换、剪辑、合并、水印添加等。本文中简鹿办公将介绍如何使用 FFmpeg 进行一些常见的音视频转换任务。 安装…