设计模式探索:观察者模式
1. 观察者模式
1.1 什么是观察者模式
观察者模式用于建立一种对象与对象之间的依赖关系,当一个对象发生改变时将自动通知其他对象,其他对象会相应地作出反应。

在观察者模式中有如下角色:
- Subject(抽象主题/被观察者): 抽象主题角色把所有观察者对象保存在一个集合里,每个主题可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
- ConcreteSubject(具体主题/具体被观察者): 该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
- Observer(抽象观察者): 观察者的抽象类,定义了一个更新接口,使得在得到主题更改通知时更新自己。
- ConcreteObserver(具体观察者): 实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。在具体观察者中维护一个指向具体目标对象的引用,存储具体观察者的有关状态,这些状态需要与具体目标保持一致。
1.2 观察者模式实现
- 观察者
/*** 抽象观察者*/
public interface Observer {// update方法: 为不同观察者的更新(响应)行为定义相同的接口,不同的观察者对该方法有不同的实现void update();
}/*** 具体观察者*/
public class ConcreteObserverOne implements Observer {@Overridepublic void update() {// 获取消息通知,执行业务代码System.out.println("ConcreteObserverOne 得到通知!");}
}/*** 具体观察者*/
public class ConcreteObserverTwo implements Observer {@Overridepublic void update() {// 获取消息通知,执行业务代码System.out.println("ConcreteObserverTwo 得到通知!");}
}
- 被观察者
/*** 抽象目标类*/
public interface Subject {void attach(Observer observer);void detach(Observer observer);void notifyObservers();
}/*** 具体目标类*/
public class ConcreteSubject implements Subject {// 定义集合,存储所有观察者对象private ArrayList<Observer> observers = new ArrayList<>();// 注册方法,向观察者集合中增加一个观察者@Overridepublic void attach(Observer observer) {observers.add(observer);}// 注销方法,用于从观察者集合中删除一个观察者@Overridepublic void detach(Observer observer) {observers.remove(observer);}// 通知方法@Overridepublic void notifyObservers() {// 遍历观察者集合,调用每一个观察者的响应方法for (Observer obs : observers) {obs.update();}}
}
- 测试类
public class Client {public static void main(String[] args) {// 创建目标类(被观察者)ConcreteSubject subject = new ConcreteSubject();// 注册观察者类,可以注册多个subject.attach(new ConcreteObserverOne());subject.attach(new ConcreteObserverTwo());// 具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。subject.notifyObservers();}
}
2. 发布订阅模式与观察者模式的区别
2.1 定义上的不同
发布订阅模式属于广义上的观察者模式。
- 发布订阅模式是最常用的一种观察者模式的实现,从解耦和重用角度来看,更优于典型的观察者模式。
2.2 两者的区别
我们来看一下观察者模式与发布订阅模式结构上的区别

操作流程上的区别
- 观察者模式:数据源直接通知订阅者发生改变。
- 发布订阅模式:数据源告诉第三方(事件通道)发生了改变,第三方再通知订阅者发生了改变。
3. 观察者模式在实际开发中的应用
3.1 实际开发中的需求场景
在我们日常业务开发中,观察者模式的一个重要作用在于实现业务的解耦。以用户注册的场景为例,假设在用户注册完成时,需要给该用户发送邮件、发送优惠券等操作,如下图所示:

使用观察者模式之后

- UserService 在完成自身的用户注册逻辑之后,仅需要发布一个 UserRegisterEvent 事件,而无需关注其它拓展逻辑。
- 其它 Service 可以自己订阅 UserRegisterEvent 事件,实现自定义的拓展逻辑。
3.2 Spring事件机制
Spring 基于观察者模式,实现了自身的事件机制,由三部分组成:

- 事件
ApplicationEvent:通过继承它,实现自定义事件。另外,通过它的source属性可以获取事件源,timestamp属性可以获得发生时间。 - 事件发布者
ApplicationEventPublisher:通过它,可以进行事件的发布。 - 事件监听器
ApplicationListener:通过实现它,进行指定类型的事件的监听。
3.3 代码实现
(1) UserRegisterEvent
- 创建
UserRegisterEvent事件类,继承ApplicationEvent类,用户注册事件。代码如下:
/*** 用户注册事件*/
public class UserRegisterEvent extends ApplicationEvent {private String username;public UserRegisterEvent(Object source) {super(source);}public UserRegisterEvent(Object source, String username) {super(source);this.username = username;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}
}
(2) UserService (事件源+事件发布)
- 创建
UserService类,代码如下:
/*** 事件源角色+事件发布*/
@Service
public class UserService implements ApplicationEventPublisherAware {private Logger logger = LoggerFactory.getLogger(getClass());private ApplicationEventPublisher applicationEventPublisher;@Overridepublic void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {this.applicationEventPublisher = applicationEventPublisher;}public void register(String username){// 执行注册逻辑logger.info("[register][执行用户{}的注册逻辑]", username);// 发布用户注册事件applicationEventPublisher.publishEvent(new UserRegisterEvent(this, username));}
}
- 实现
ApplicationEventPublisherAware接口,从而将ApplicationEventPublisher注入到其中。 - 在执行完注册逻辑后,调用
ApplicationEventPublisher的publishEvent(ApplicationEvent event)方法,发布UserRegisterEvent事件。
(3) 创建 EmailService
/*** 事件监听角色*/
@Service
public class EmailService implements ApplicationListener<UserRegisterEvent> {private Logger logger = LoggerFactory.getLogger(getClass());@Overridepublic void onApplicationEvent(UserRegisterEvent event) {logger.info("[onApplicationEvent][给用户({}) 发送邮件]", event.getUsername());}
}
- 实现
ApplicationListener接口,通过E泛型设置感兴趣的事件。 - 实现
onApplicationEvent(E event)方法,针对监听的UserRegisterEvent事件,进行自定义处理。
(4) CouponService
@Service
public class CouponService {private Logger logger = LoggerFactory.getLogger(getClass());@EventListener public void addCoupon(UserRegisterEvent event) {logger.info("[addCoupon][给用户({}) 发放优惠劵]", event.getUsername());}
}
- 添加
@EventListener注解,并设置监听的事件为UserRegisterEvent。
(5) DemoController
- 提供
/demo/register注册接口
@RestController
@RequestMapping("/demo")
public class DemoController {@Autowiredprivate UserService userService;@GetMapping("/register")public String register(String username) {userService.register(username);return "success";}
}
3.4 代码测试
- 执行
DemoApplication类,启动项目。 - 调用
http://127.0.0.1:8080/demo/register?username=mashibing接口,进行注册。IDEA 控制台打印日志如下:
// UserService 发布 UserRegisterEvent 事件
2023-04-19 16:49:40.628 INFO 9800 --- [nio-8080-exec-1] c.m.d.o.demo02.service.UserService : [register][执行用户mashibing的注册逻辑]// EmailService 监听处理该事件
2023-04-19 16:49:40.629 INFO 9800 --- [nio-8080-exec-1] c.m.d.o.demo02.listener.EmailService : [onApplicationEvent][给用户(mashibing) 发送邮件]// CouponService 监听处理该事件
2023-04-19 16:49:40.629 INFO 9800 --- [nio-8080-exec-1] c.m.d.o.demo02.listener.CouponService : [addCoupon][给用户(mashibing) 发放优惠劵]
4. 观察者模式总结
1) 观察者模式的优点
- 降低目标类和观察者之间的耦合
- 可以实现广播机制
2) 观察者模式的缺点
- 通知的发送会消耗一定的时间
- 如果被观察者有循环依赖,会导致系统的崩溃
3) 观察者模式常见的使用场景
- 一个对象的改变,需要改变其他对象的时候
- 一个对象的改变,需要进行通知的时候
相关文章:
设计模式探索:观察者模式
1. 观察者模式 1.1 什么是观察者模式 观察者模式用于建立一种对象与对象之间的依赖关系,当一个对象发生改变时将自动通知其他对象,其他对象会相应地作出反应。 在观察者模式中有如下角色: Subject(抽象主题/被观察者…...
Perl语言入门到高级学习
Perl语言介绍 Perl,全称为Practical Extraction and Report Language,即“实用报表提取语言”,是一种高级、通用、直译式、动态的编程语言。Perl最初由Larry Wall设计,并于1987年12月18日首次发布。经过多年的不断发展和更新,Perl已经成为一种功能丰富且应用广泛的计算机程…...
DOM 基本操作 - 获取元素
theme: smartblue 一、简介 1.1 概念 文档对象模型(Document Object Model),是 W3C 组织推荐的处理可拓展标记语言的标准编程接口。 1.2 DOM 树 二、 获取元素 获取页面中的元素主要可以使用以几种方式: - 根据 ID 获取 - 根据 标签名 获取 - 通过 HTML5 新增的方法…...
Google 搜索引擎:便捷高效、精准查询,带来无与伦比的搜索体验
Google搜索引擎不仅具备检索功能,实则是引领探索万千世界的神秘钥匙。试想,无论何时何地,只需轻触屏幕,所需信息即可唾手可得。便捷与高效,令人叹为观止。其界面设计简约直观,操控体验犹如与未来对话&#…...
tomcat的介绍与优化
tomcat介绍 tomcat和php一样,都是用来处理动态页面的。 tomcat也可以作为web应用服务器,开源的。 php .php tomcat .jsp nginx .html tomcat 是用java代码写的程序,运行的是javaweb应用程序 tomcat的特点和功能: 1.servlet容器…...
Python 插入、替换、提取、或删除Excel中的图片
Excel是主要用于处理表格和数据的工具,我们也能在其中插入、编辑或管理图片,为工作表增添视觉效果,提升报告的吸引力。本文将详细介绍如何使用Python操作Excel中的图片,包含以下4个基础示例: 文章目录 Python 在Excel…...
紧凑型建模的veriloga语句要怎么看?
说点人话,真传一句话,那些一堆公式似是而非的东西,都是半懂不懂的人沽名钓誉用的。 其实建模,归根结底明白几个东西就行了。 1.什么是你的输入和输出信号? 2.你对输入输出信号要建立什么功能关系? 那我们看…...
大语言模型系列-Transformer介绍
大语言模型系列:Transformer介绍 引言 在自然语言处理(NLP)领域,Transformer模型已经成为了许多任务的标准方法。自从Vaswani等人在2017年提出Transformer以来,它已经彻底改变了NLP模型的设计。本文将介绍Transforme…...
JavaDS —— 顺序表ArrayList
顺序表 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。在物理和逻辑上都是连续的。 模拟实现 下面是我们要自己模拟实现的方法: 首先我们要创建一个顺序表,顺序表…...
Sphinx 搜索配置
官方文档 http://sphinxsearch.com/docs/sphinx3.html 支持中文,英文,日文,韩文,俄罗斯语搜索 版本是 官网3.6.1版本 文件 sphinx.conf.dist 的windows 配置,官网下载下来后微微配置即可。 # Minimal Sphinx confi…...
如何在不关闭防火墙的情况下,让两台设备ping通
问题现象 如题,做虚拟机实验的时候,有一台linux系统的虚拟机配置的ip地址是192.168.172.181 物理主机的ip地址是192.168.172.1 此时物理主机可以ping通虚拟机 但是虚拟机不能ping通物理主机 此时我们可以想到,有可能是物理主机防火墙的原因。…...
windows USB 设备驱动开发-USB 等时传输
客户端驱动程序可以生成 USB 请求块 (URB) 以在 USB 设备中向/从常时等量端点传输数据。虽然USB设备一向以非等时传输出名,USB提供的是一种串行数据,而非等时,但是USB仍然设计了等时传输的机制,但根据笔者的经验,等时传…...
【文件共享 windows和linux】Windows Server 2016上开启文件夹共享,并在CentOS 7.4上访问和下载文件
要在Windows Server 2016上开启文件夹共享,并在CentOS 7.4上访问和下载文件,请按照以下步骤操作: 在Windows Server 2016上开启文件夹共享: 启用SMB服务: 打开“服务器管理器”。选择“文件和存储服务” > “共享…...
【知网CNKI-注册安全分析报告】
前言 由于网站注册入口容易被黑客攻击,存在如下安全问题: 暴力破解密码,造成用户信息泄露短信盗刷的安全问题,影响业务及导致用户投诉带来经济损失,尤其是后付费客户,风险巨大,造成亏损无底洞…...
【Python_GUI】tkinter常用组件——文本类组件
文本时窗口中必不可少的一部分,tkinter模块中,有3种常用的文本类组件,通过这3种组件,可以在窗口中显示以及输入单行文本、多行文本、图片等。 Label标签组件 Label组件的基本使用 Label组件是窗口中比较常用的组件,…...
zdppy+onlyoffice+vue3解决文档加载和文档强制保存时弹出警告的问题
解决过程 第一次排查 最开始排查的是官方文档说的 https://api.onlyoffice.com/editors/troubleshooting#key 解决方案。参考的是官方的 https://github.com/ONLYOFFICE/document-server-integration/releases/latest/download/Python.Example.zip 基于Django的Python代码。 …...
C语言从头学31——与字符串变量相关的几个函数
strlen、strcpy、strcat、strcmp、sprintf这些函数都是与字符串相关的,除了sprintf是定义在stdio.h中外,其余几个都定义在string.h中,比较新的编译器版本stdio.h中已经含有string.h的内容,所以编程时不需要再包含string.h这个头文…...
Laravel批量插入数据:提升数据库操作效率的秘诀
Laravel批量插入数据:提升数据库操作效率的秘诀 Laravel作为PHP的现代Web应用框架,提供了优雅而简洁的方法来处理数据库操作。批量插入数据是数据库操作中常见的需求,尤其是在处理大量数据时,批量插入可以显著提高性能。本文将详…...
OpenCV:解锁计算机视觉的魔法钥匙
OpenCV:解锁计算机视觉的魔法钥匙 在人工智能与图像处理的世界里,OpenCV是一个响当当的名字。作为计算机视觉领域的瑞士军刀,OpenCV以其丰富的功能库、跨平台的特性以及开源的便利性,成为了开发者手中不可或缺的工具。本文将深入…...
手写简单模拟mvc
目录结构: 两个注解类: Controller: package com.heaboy.annotation;import java.lang.annotation.*;/*** 注解没有功能只是简单标记* .RUNTIME 运行时还能看到* .CLASS 类里面还有,构建对象久没来了,这个说明…...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...
微信小程序云开发平台MySQL的连接方式
注:微信小程序云开发平台指的是腾讯云开发 先给结论:微信小程序云开发平台的MySQL,无法通过获取数据库连接信息的方式进行连接,连接只能通过云开发的SDK连接,具体要参考官方文档: 为什么? 因为…...
select、poll、epoll 与 Reactor 模式
在高并发网络编程领域,高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表,以及基于它们实现的 Reactor 模式,为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。 一、I…...
OPenCV CUDA模块图像处理-----对图像执行 均值漂移滤波(Mean Shift Filtering)函数meanShiftFiltering()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 在 GPU 上对图像执行 均值漂移滤波(Mean Shift Filtering),用于图像分割或平滑处理。 该函数将输入图像中的…...
Go 语言并发编程基础:无缓冲与有缓冲通道
在上一章节中,我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道,它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好࿰…...
C# 表达式和运算符(求值顺序)
求值顺序 表达式可以由许多嵌套的子表达式构成。子表达式的求值顺序可以使表达式的最终值发生 变化。 例如,已知表达式3*52,依照子表达式的求值顺序,有两种可能的结果,如图9-3所示。 如果乘法先执行,结果是17。如果5…...
MySQL 索引底层结构揭秘:B-Tree 与 B+Tree 的区别与应用
文章目录 一、背景知识:什么是 B-Tree 和 BTree? B-Tree(平衡多路查找树) BTree(B-Tree 的变种) 二、结构对比:一张图看懂 三、为什么 MySQL InnoDB 选择 BTree? 1. 范围查询更快 2…...
如何应对敏捷转型中的团队阻力
应对敏捷转型中的团队阻力需要明确沟通敏捷转型目的、提升团队参与感、提供充分的培训与支持、逐步推进敏捷实践、建立清晰的奖励和反馈机制。其中,明确沟通敏捷转型目的尤为关键,团队成员只有清晰理解转型背后的原因和利益,才能降低对变化的…...
WPF八大法则:告别模态窗口卡顿
⚙️ 核心问题:阻塞式模态窗口的缺陷 原始代码中ShowDialog()会阻塞UI线程,导致后续逻辑无法执行: var result modalWindow.ShowDialog(); // 线程阻塞 ProcessResult(result); // 必须等待窗口关闭根本问题:…...
Qt的学习(二)
1. 创建Hello Word 两种方式,实现helloworld: 1.通过图形化的方式,在界面上创建出一个控件,显示helloworld 2.通过纯代码的方式,通过编写代码,在界面上创建控件, 显示hello world; …...
