观察者模式(Observer)

news2024/11/17 11:26:47

别名

  • 事件订阅者者(Event-Subscriber)
  • 监听者(Listener)

定义

观察者是一种行为设计模式,允许你定义一种订阅机制,可在对象事件发生时通知多个“观察”该对象的其他对象。

前言

1. 问题

假如你有两种类型的对象:“顾客”和“商店”。顾客对某个特定品牌的产品非常感兴趣(例如最新型号的 iPhone 手机),而该产品很快将会在商店里出售

  • 一方面顾客可以每天来商店看看产品是否到货。但如果商品尚未到货时,绝大多数来到商店的顾客都会空手而归。
  • 另一方面,每次新产品到货时,商店可以向所有顾客发送邮件(可能会被视为垃圾邮件)。这样,部分顾客就无需反复前往商店了,但也可能会惹恼对新产品没有兴趣的其他顾客。

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

2. 解决方案

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

观察者模式建议你为发布者类添加订阅机制,让每个对象都能订阅或取消订阅发布者事件流。不要害怕!这并不像听上去那么复杂。实际上,该机制包括

  1. 一个用于存储订阅者对象引用的列表成员变量。
  2. 几个用于添加或删除该列表中订阅者的公有方法。

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

实际应用中可能会有十几个不同的订阅者类跟踪着同一个发布者类的事件, 你不会希望发布者与所有这些类相耦合的。 此外如果他人会使用发布者类,那么你甚至可能会对其中的一些类一无所知。因此,所有订阅者都必须实现同样的接口,发布者仅通过该接口与订阅者交互。接口中必须声明通知方法及其参数,这样发布者在发出通知时还能传递一些上下文数据。

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

结构

  1. 发布者(Publisher)会向其他对象发送值得关注的事件。事件会在发布者自身状态改变或执行特定行为后发生。发布者中包含一个允许新订阅者加入和当前订阅者离开列表的订阅构架。
  2. 当新事件发生时,发送者会遍历订阅列表并调用每个订阅者对象的通知方法。该方法是在订阅者接口中声明的。
  3. 订阅者(Subscriber)接口声明了通知接口。 在绝大多数情况下,该接口仅包含一个 update方法。该方法可以拥有多个参数,使发布者能在更新时传递事件的详细信息。
  4. 具体订阅者(Concrete Subscribers)可以执行一些操作来回应发布者的通知。 所有具体订阅者类都实现了同样的接口,因此发布者不需要与具体类相耦合。
  5. 订阅者通常需要一些上下文信息来正确地处理更新。 因此,发布者通常会将一些上下文数据作为通知方法的参数进行传递。发布者也可将自身作为参数进行传递,使订阅者直接获取所需的数据。
  6. 客户端(Client)会分别创建发布者和订阅者对象,然后为订阅者注册发布者更新。

适用场景

  • 当一个对象状态的改变需要改变其他对象,或实际对象是事先未知的或动态变化的时,可使用观察者模式。

当你使用图形用户界面类时通常会遇到一个问题。比如,你创建了自定义按钮类并允许客户端在按钮中注入自定义代码,这样当用户按下按钮时就会触发这些代码。观察者模式允许任何实现了订阅者接口的对象订阅发布者对象的事件通知。你可在按钮中添加订阅机制,允许客户端通过自定义订阅类注入自定义代码。

  • 当应用中的一些对象必须观察其他对象时,可使用该模式。但仅能在有限时间内或特定情况下使用。

订阅列表是动态的,因此订阅者可随时加入或离开该列表。

实现方式

  1. 仔细检查你的业务逻辑,试着将其拆分为两个部分:独立于其他代码的核心功能将作为发布者;其他代码则将转化为一组订阅类。
  2. 声明订阅者接口。该接口至少应声明一个 update 方法。
  3. 声明发布者接口并定义一些接口来在列表中添加和删除订阅对象。记住发布者必须仅通过订阅者接口与它们进行交互。
  4. 确定存放实际订阅列表的位置并实现订阅方法。通常所有类型的发布者代码看上去都一样,因此将列表放置在直接扩展自发布者接口的抽象类中是显而易见的。具体发布者会扩展该类从而继承所有的订阅行为。但是,如果你需要在现有的类层次结构中应用该模式,则可以考虑使用组合的方式: 将订阅逻辑放入一个独立的对象,然后让所有实际订阅者使用该对象。
  5. 创建具体发布者类。每次发布者发生了重要事件时都必须通知所有的订阅者。
  6. 在具体订阅者类中实现通知更新的方法。绝大部分订阅者需要一些与事件相关的上下文数据。这些数据可作为通知方法的参数来传递。但还有另一种选择。订阅者接收到通知后直接从通知中获取所有数据。在这种情况下,发布者必须通过更新方法将自身传递出去。另一种不太灵活的方式是通过构造函数将发布者与订阅者永久性地连接起来。
  7. 客户端必须生成所需的全部订阅者,并在相应的发布者处完成注册工作。

优点

  • 开闭原则。 你无需修改发布者代码就能引入新的订阅者类(如果是发布者接口则可轻松引入发布者类)。
  • 你可以在运行时建立对象之间的联系。

缺点

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

Publisher.hpp

#ifndef FD172440_394F_4863_9CAE_F8560CE768A8
#define FD172440_394F_4863_9CAE_F8560CE768A8

#include <vector>
#include <iostream>
#include "Subscriber.hpp"

class Cat {
 public:
    // 注册观察者
    void attach(AbstractObserver* observer) {
        observers_.push_back(observer);
    }

    // 注销观察者
    void detach(AbstractObserver* observer) {
        for (auto it = observers_.begin(); it !=observers_.end(); it++) {
            if (*it == observer) {
                observers_.erase(it);
                break;
            }
        }
    }

    void cry() {
        std::cout << "猫叫!" << std::endl;
        for (auto ob : observers_) {
            ob->response();
        }
    }

 private:
    std::vector<AbstractObserver*> observers_;
};

#endif /* FD172440_394F_4863_9CAE_F8560CE768A8 */

Subscriber.hpp

#ifndef CCE3E5F5_BD59_43AE_9BCA_E13909DD792C
#define CCE3E5F5_BD59_43AE_9BCA_E13909DD792C

class AbstractObserver {
    public:
        virtual void response() = 0;
};

#endif /* CCE3E5F5_BD59_43AE_9BCA_E13909DD792C */

ConcreteSubscriber.hpp

#ifndef F648013D_8238_484B_83AC_2D374CD2D65E
#define F648013D_8238_484B_83AC_2D374CD2D65E

#include <iostream>
#include "Subscriber.hpp"

// 具体观察者1: 老鼠
class Mouse : public AbstractObserver {
 public:
    void response() override {
        std::cout << "老鼠逃跑" << std::endl;
    }
};

// 具体观察者2: 狗
class Dog : public AbstractObserver {
 public:
    void response() override {
        std::cout << "狗追猫" << std::endl;
    }
};
#endif /* F648013D_8238_484B_83AC_2D374CD2D65E */

main.cpp

#include "Publisher.hpp"
#include "ConcreteSubscriber.hpp"

int main() {
    // 发布者
    Cat cat;

    // 观察者
    Mouse mouse;
    Dog dog;

    // 添加订阅关系
    cat.attach(&mouse);
    cat.attach(&dog);

    // 发布消息
    cat.cry();
    return 0;
}

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

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

相关文章

Linux vfs各种operation操作介绍

1.ext4文件系统定义的各种操作 //普通文件操作 const struct file_operations ext4_file_operations {.llseek ext4_llseek,.read_iter generic_file_read_iter,.write_iter ext4_file_write_iter,.unlocked_ioctl ext4_ioctl, #ifdef CONFIG_COMPAT.compat_ioctl …

【Python基础】- break和continue语句(文末送书4本)

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

6 图像处理实现螺纹识别案例(matlab程序)

学习目的&#xff1a;学习识别案例掌握识别方法 2.代码 clear;clc;close all Iimread(luowen1.bmp); %读取螺纹图片 try Irgb2gray(I); %如果是RGB图&#xff0c;则转换成灰度图 catch end figure imshow(I) title(原图&#xff08;半边螺纹&#xff09;) f…

基于pyqt和卷积网络CNN的中文汉字识别

直接上效果演示图&#xff1a; 通过点击按钮可以实现在画板上写汉字识别和加载图片识别两个功能。 视频演示和demo仓库地址在b站视频001期&#xff1a; 到此一游7758258的个人空间-到此一游7758258个人主页-哔哩哔哩视频 所有代码展示&#xff1a; 十分的简洁&#xff0c;主…

【从零开始学习JAVA | 第二十六篇】泛型补充知识

目录 前言&#xff1a; 泛型的更多应用&#xff1a; 泛型类&#xff1a; 泛型方法&#xff1a; 泛型方法&#xff1a; 总结&#xff1a; 前言&#xff1a; 在上一篇文章中我们介绍了泛型的基础知识&#xff0c;也就是在创建集合的时候用到泛型&#xff0c;此时的泛型更多…

关于内存颗粒的地址映射

即便从软件角度&#xff0c;抛开地址译码器讨论内存颗粒中指定位置处的地址&#xff08;DDR中的指定位置的电容&#xff09;也是没有意义的。晶体管没有绝对地址&#xff0c;就如同地理测量中测定位置前需要确定坐标系一样&#xff0c;同一个位置在不同的坐标系中的地址描述可以…

【CVRP测评篇】 算法性能如何?来测!

我跨越了2100015秒的距离&#xff0c;为你送上更全面的算法性能评测。 目录 往期优质资源1 CVRP数据集2 实验准备2.1 计算机配置2.2 调参方法2.3 参数设定2.4 实验方法 3 实验结果3.1 最优解统计3.1.1各数据集上的算法性能对比3.1.2 求解结果汇总3.1.3小结一下3.1.4 还有话说 3…

使用 RedisTemplate 对象的 opsForValue() 方法获取 Redis 中的值获取不到

问题 使用 RedisTemplate 对象的 opsForValue() 方法获取 Redis 中的值获取不到 详细问题 笔者代码如下 1 使用 ValueOperations 对象的 set() 方法将一个键值对存储到 Redis 中 valueOperations.set("order:" user.getId() ":" goods.getId(), sec…

【Redis】2、Redis应用之【根据 Session 和 Redis 进行登录校验和发送短信验证码】

目录 一、基于 Session 实现登录(1) 发送短信验证码① 手机号格式后端校验② 生成短信验证码 (2) 短信验证码登录、注册(3) 登录验证① 通过 SpringMVC 定义拦截器② ThreadLocal (4) 集群 Session 不共享问题 二、基于 Redis 实现共享 session 登录(1) 登录之后&#xff0c;缓…

23款迈巴赫S480升级原厂10°后轮转向系统,减少转弯半径

就是低速的情况下&#xff0c;有更强的机动性&#xff0c;前后车轮的不同转动方向使得车辆可以凭借更更小转弯半径实现转向&#xff0c;在特定的狭窄路段或者停车时&#xff0c;车辆的操控性大大提升&#xff0c;而内轮差也缩小也增大了转向的安全性。 高速的情况下&#xff0…

C. Road Optimization(dp)

Problem - 1625C - Codeforces 火星政府不仅对优化太空飞行感兴趣&#xff0c;还希望改进该行星的道路系统。 火星上最重要的高速公路之一连接着奥林匹克城和西多尼亚的首都Kstolop。在这个问题中&#xff0c;我们只考虑从Kstolop到奥林匹克城的路线&#xff0c;而不考虑相反的…

技术创举!比亚迪-汉上的实景三维导航...

实景三维技术的发展日新月异&#xff0c;但在应用中却一直深陷内存占用、渲染缓慢、加载卡顿和模型塌陷等问题。对此&#xff0c;大势智慧率先推出海量数据轻量化技术&#xff0c;在业内首次实现实景三维模型在车机系统的直接浏览&#xff0c;展示了轻量化技术赋能实景三维应用…

面试Dubbo ,却问我和Springcloud有什么区别?

Dubbo 、Springcloud? 这两有关系&#xff1f; 前言一、RPC 框架的概念1. 什么是RPC框架2. RPC 和 普通通信 的区别 二、常用 RPC 框架1. Dubbo2. gRPC3. Thrift4. Feign 三、dubbo 与 Springcloud1. Dubbo 的模型2. Springcloud3. dubbo 与 Springcloud 的区别 前言 提到Dub…

若隐若现的芯片

先看效果&#xff1a; 再看代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>若隐若现的芯片</title><script src"https://unpkg.co/gsap3/dist/gsap.min.js">…

HBase(5):导入测试数据集

1 需求 将ORDER_INFO.txt 中的HBase数据集&#xff0c;我们需要将这些指令放到HBase中执行&#xff0c;将数据导入到HBase中。 可以看到这些都是一堆的put语句。那么如何才能将这些语句全部执行呢&#xff1f; 2 执行command文件 2.1 上传command文件 将该数据集文件上传到指…

单点登录原理

单点登录原理 一、什么是单点登录 单点登录英文全称Single Sign On&#xff0c;简称SSO。指在多系统应用群中登录一个系统&#xff0c;便可在其他所有系统中得到授权而无需再次登录&#xff0c;包括单点登录与单点注销两部分。 二、为什么需要单点登录 在一些子系统用户信息…

初识Go语言25-数据结构与算法【堆、Trie树、用go中的list与map实现LRU算法、用go语言中的map和堆实现超时缓存】

文章目录 堆Trie树练习-用go中的list与map实现LRU算法练习-用go语言中的map和堆实现超时缓存 堆 堆是一棵二叉树。大根堆即任意节点的值都大于等于其子节点。反之为小根堆。   用数组来表示堆&#xff0c;下标为 i 的结点的父结点下标为(i-1)/2&#xff0c;其左右子结点分别为…

Python图像锐化及边缘检测(Roberts、Prewitt、Sobel、Lapllacian、Canny、LOG)

目录 图像锐化概述 算法方法介绍 代码实现 效果展示 图像锐化概述 图像锐化(image sharpening)是补偿图像的轮廓&#xff0c;增强图像的边缘及灰度跳变的部分&#xff0c;使图像变得清晰&#xff0c;分为空间域处理和频域处理两类。图像锐化是为了突出图像上地物的边缘、轮…

Docker|kubernetes|本地镜像批量推送到Harbor私有仓库的脚本

前言&#xff1a; 可能有测试环境&#xff0c;而测试环境下有N多的镜像&#xff0c;需要批量导入到自己搭建的Harbor私有仓库内&#xff0c;一般涉及到批量的操作&#xff0c;自然还是使用脚本比较方便。 本文将介绍如何把某个服务器的本地镜像 推送到带有安全证书的私有Harb…

【P61】JMeter JDBC Connection Configuration

文章目录 一、JDBC Connection Configuration 参数说明二、准备工作 一、JDBC Connection Configuration 参数说明 可以给数据源配置不同的连接池&#xff0c;供后续 JDBC 采样器使用&#xff1b;使用前请将对应的数据库驱动复制到 $JMETER_HOME/lib/ 或者 $JMETER_HOME/lible…