【设计模式】循序渐进的理解观察者模式Spring事件机制的运用

news2024/12/24 8:46:10

文章目录

  • 1. 概述
  • 2.循序渐进的理解观察者模式
    • 2.1 观察者模式概念引入
    • 2.2.观察者接口抽象
    • 2.3 被观察者接口抽象
    • 2.4 观察者模式的通用类图
    • 2.5.观察者模式的通用代码实现
  • 3.Spring中的事件运用
    • 3.1.Spring事件中的几个角色介绍
    • 3.2.代码实现
  • 4.总结

1. 概述

观察者模式(Observer Pattern) 是一种应用广泛的设计模式,有时候也被叫做发布/订阅模式或者监听器模式,如果你用过消息队列或者写过原生JS和JQuery中的Listener的话,对这种模式一定不陌生,接下来你会非常容易理解这种模式,如果没有使用过也没关系可以通过本文来了解一下这种模式的运用。

本文中会通过大量的类图来描述观察者模式中的不同角色,对类型不太熟悉的同学可以参考者这篇文章一起看《【UML建模】类图(Class Diagram)》

2.循序渐进的理解观察者模式

2.1 观察者模式概念引入

观察者模式就是一个对象的内部状态发生改变时(就是做了点事),依赖它的一个或多个对象会得到通知并自动更新状态(就是跟着做点事),定义上就是这么简单。

我们先看一下观察者模式中的两种角色:观察者被观察者
在这里插入图片描述
观察者在被观察者doSomething()的时候,自动的触发doSomething()应该怎么做呢?

最简单的做法就是被观察者执行doSomething()时,调用一下观察者的doSomething()方法,也就是最常见的方法调用。从这里可以看出,所谓的自动更新并不是观察者主观上的自动运行,而是依赖于其他对象的触发。
这种触发机制我们一般把它叫做事件机制,在软件工程中大量运用,例如使用Jenkins中的CICD配置当git提交时自动构建服务到测试环境就是一种体现。

2.2.观察者接口抽象

那上述的这种方法调用就叫做观察者模式了?这和平时写的代码也没啥区别嘛!

当然不仅仅是这样的,我们顺着这个类图分析,假设我们现在新增两个观察者,类图就变成了这样:
在这里插入图片描述
可以想象一下,如果这个时候需要触发所有观察者的方法,就需要写3次方法调用,分别调用3个不同的观察者,但假如有10个、20个观察者呢?

针对这种情况,我们当然不可能去写20次方法调用,更好的方式是事先将所有观察者都放到集合中,当被观察者执行了指定的方法时,直接循环这个集合依次触发就好了。

要想让所有的观察者都可以放入到同一个集合中,我们需要对观察者做抽象,定义一个观察者接口,类图如下:
在这里插入图片描述

2.3 被观察者接口抽象

现在我们已经有了观察者集合,接下来就是对这个集合的操作,相信大家很容易就可以想到至少应该包含3种不同的操作:

  • 新增集合元素:可动态的加载观察者
  • 移除集合元素:可动态的卸载观察者
  • 循环集合元素触发事件:触发观察者更新状态

在实际的业务中,不同的业务流程中会定义不同的被观察者,每个被观察者都有上述的集合以及3个操作,这就需要我们将其提取到父类中。
抽取共性中的实例方法一般使用抽象类(Java8以后也可以使用Interface中的default方法),最终形成了下面的类图:
在这里插入图片描述
执行流程如下:

1.创建观察者对象
2.创建被观察者对象,并将观察者add到集合中
3.调用被观察者的doSomething
4.被观察者调用父类中的trigger,循环集合获取观察者,并调用观察者中的doSomething
5.如果不需要某个观察者后,通过remove将其从集合中移除,后续就不再触发这个观察者的doSomething方法了

2.4 观察者模式的通用类图

根据上面的分析,我们可以得出观察者模式的4种角色,分别为:

  • 观察者(Observer):上述的观察者接口
  • 被观察者(Subject):被观察者抽象类
  • 具体的观察者(Concrete Observer):观察者实例
  • 具体的被观察者(Concrete Subject):被观察者实例

综上,可以得出观察者模式的通用类图:
在这里插入图片描述
需注意的是,trigger方法不一定是空参,也可以根据业务的需要传输几个必要的参数,观察者可以根据获取到的参数采取不同针对性的处理。


再插一句题外话,由于观察者的生命周期并不需要与被观察者一直(即可以运行时动态的增减),所以此处的关联关系采用聚合而不是组合

2.5.观察者模式的通用代码实现

通过上面的通用类图,写一个通用代码的实现,并验证在运行时动态的移除观察者。

观察者: 提供一个观察者接口与两个实现类

/**
 * 观察者接口
 */
public interface Observer {
    void doSomething(String msg);
}

/**
 * 观察者实现1
 */
public class ConcreteObserver1 implements Observer {
    @Override
    public void doSomething(String msg) {
        System.out.println("触发观察者1,参数:" + msg);
    }
}

/**
 * 观察者实现2
 */
public class ConcreteObserver2 implements Observer {
    @Override
    public void doSomething(String msg) {
        System.out.println("触发观察者2,参数:" + msg);
    }
}

被观察者: 提供一个抽象父类与一个被观察者实现

/**
 * 被观察者抽象类
 */
public abstract class Subject {

    List<Observer> observerList = new ArrayList<>();

    public void addObserver(Observer observer) {
        observerList.add(observer);
    }

    public void removeObserver(Observer observer) {
        observerList.remove(observer);
    }

    public void trigger(String msg) {
        for (Observer observer : observerList) {
            observer.doSomething(msg);
        }
    }
}
/**
 * 被观察者实现
 */
public class ConcreteSubject extends Subject {
    public void doSomething() {
        System.out.println("被观察者执行方法");
        String msg = "被观察者执行方法";
        super.trigger(msg);
    }
}

随便写个main方法做个测试

public class Test {

    public static void main(String[] args) {
        ConcreteSubject subject = new ConcreteSubject();
        ConcreteObserver1 observer1 = new ConcreteObserver1();
        ConcreteObserver2 observer2 = new ConcreteObserver2();

        subject.addObserver(observer1);
        subject.addObserver(observer2);

        System.out.println("-----第一次运行-----");
        subject.doSomething();

        System.out.println("移除观察者1");
        subject.removeObserver(observer1);
        System.out.println("-----第二次运行-----");
        subject.doSomething();
    }
}

在这里插入图片描述

3.Spring中的事件运用

虽然我们在日常的开发过程中也能使用上面的通用代码来实现观察者模式,但如果在开发的过程中使用了Spring框架的话,我们也可以使用Spring中的事件机制(Event) 来替代上述的通用代码。

Spring的事件机制其实就是对观察者模式的封装,达到更加通用且实现更加简单的效果。

3.1.Spring事件中的几个角色介绍

使用Spring的事件,其实就是通过继承、实现、聚合、依赖等方式,将Spring封装好的功能对象集成到业务代码中,在集成之前有必要先了解一下这些功能对象。

  • ApplicationEvent:用于定义事件对象,事件对象既是事件触发的标识类型,也是事件中信息传输的载体,可简单的立即为事件触发后向观察者传递的参数。
  • ApplicationListener:用于接收事件的监听器,其实就是观察者这个角色。
  • ApplicationEventPublisher:事件推送器,用于将事件推送给事件监听器(其实是推送给了事件投递器,下面有详细的解释)。
  • ApplicationEventPublisherAware:用于将事件推送器聚合到业务对象中,Spring中有很多不同的Aware接口都是这个作用,属于是Spring的扩展点。

以上是我们在编码过程中会直接使用到的角色,如果仔细对比了上面的通用类图中的角色,一定会发现这些角色还少了一部分功能,即:新增、移除观察者,以及触发方法trigger。这部分“缺失”的功能在另一个接口ApplicationEventMulticaster中,看下面的截图就明白了。
在这里插入图片描述
至于如何使用这些功能对象,可以看下面的代码实现。

3.2.代码实现

假设现在有这样一个需求:提供一个消息发送事件,当系统中完成了某些业务流程时,触发这个时间给对应的负责人发送消息,现有一个更新会员的业务功能,在功能完成时推送更新完成消息,在更新异常时,推送更新失败消息。

我们可以通过3个步骤完成这个功能:

  • 定义消息发送事件:
    import org.springframework.context.ApplicationEvent;
    
    /**
     * 消息发送事件
     */
    public class MsgSendEvent extends ApplicationEvent {
        /**
         * 需要发送的消息
         */
        private String msg;
    
        public MsgSendEvent(Object source) {
            super(source);
        }
    
        public MsgSendEvent(Object source, String msg) {
            super(source);
            this.msg = msg;
        }
    
        public String getMsg() {
            return msg;
        }
    }
    
  • 定义事件监听器:
    import org.springframework.context.ApplicationListener;
    import org.springframework.stereotype.Component;
    
    /**
     * 消息发送事件监听器
     */
    @Component
    public class MsgEventSendListener implements ApplicationListener<MsgSendEvent> {
        @Override
        public void onApplicationEvent(MsgSendEvent event) {
            Object source = event.getSource();
            System.out.println("MsgEventSendListener-触发消息发送的被观察者:" + source + ",消息内容:" + event.getMsg());
        }
    }
    
  • 在业务对象中引入Publisher,并推送事件:
    import org.springframework.context.ApplicationEventPublisher;
    import org.springframework.context.ApplicationEventPublisherAware;
    import org.springframework.stereotype.Service;
    
    @Service
    public class MemberService implements ApplicationEventPublisherAware {
    
        private ApplicationEventPublisher publisher;
    
        @Override
        public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
            this.publisher = publisher;
        }
    
        public void batchUpdate() {
            try {
                System.out.println("MemberService-批量更新数据");
                publisher.publishEvent(new MsgSendEvent(this, "数据更新完成"));
            } catch (Exception e) {
                e.printStackTrace();
                publisher.publishEvent(new MsgSendEvent(this, "数据更新异常"));
            }
        }
    }
    

调用下memberService中的batchUpdate方法做个测试:
在这里插入图片描述

在上面的代码中,我们不需要再手动的注入或移除观察者,只需要在ApplicationListener<T>的泛型中写定义好的Event类,然后再业务对象中创建对应的Event对象,ApplicationEventMulticaster就可以根据Event的类型找到对应的观察者。

在我们的实际开发中,在Listener对象中可以拓展出更多的业务逻辑,例如将消息发送改成异步,通过envent传递标识执行不同的逻辑等等,可以根据自己的业务需求和功能设计做自由的组合。毕竟,观察者的存在目的只是为了与被观察者解耦,并不局限于只做一些简单的逻辑。

4.总结

本篇讲述了观察者模式的概念及在Java中的使用方式,可以在日常开放中参照上面的使用方式来处理业务。需要注意一个点就是观察者也可以作为另一个观察者的被观察者,可以无限套娃,但为了减少后续维护的难度,套娃尽可能不要超过两层。

除了使用之外,更重要的是理解观察者模式的设计思想,在软件工程中不管是本篇所说的观察者、监听器、事件,还是在其他地方使用到的发布/订阅、触发器等,其本质都是由一个角色完成某项功能时,向其他依赖它的对象发送了一个完成通知,这些依赖对象接收到通知之后再做出一些特定的操作,仅此而已,不用考虑的过于复杂。

总之,在对观察者模式有一定的理解之后,可以发现它也是一种比较简单的设计模式,希望本篇文章的内容对大家在日后的开发中能够有所帮助。

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

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

相关文章

RabbitMQ高阶使用

1. 问题 2. 延时任务 2.1 什么是延时任务 在当前时间往后延迟多少时间执行的任务 2.1.1 和定时任务区别 定时任务有明确的触发时间&#xff0c;延时任务没有定时任务有执行周期&#xff0c;而延时任务在某事件触发后一段时间内执行&#xff0c;没有执行周期定时任务一般执行的…

生命周期函数和wxs脚本

生命周期函数和wxs脚本 1. 生命周期函数1.1. 应用的生命周期函数1.2. 页面的生命周期函数 2. wxs脚本2.1. wxs与JavaScript的关系2.2. wxs内嵌脚本2.3. wxs外联脚本2.4. tips 1. 生命周期函数 1.1. 应用的生命周期函数 应用的生命周期函数&#xff1a;指小程序从启动 -> 运…

【每日算法】【226. 翻转二叉树】

☀️博客主页&#xff1a;CSDN博客主页 &#x1f4a8;本文由 我是小狼君 原创&#xff0c;首发于 CSDN&#x1f4a2; &#x1f525;学习专栏推荐&#xff1a;面试汇总 ❗️游戏框架专栏推荐&#xff1a;游戏实用框架专栏 ⛅️点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd;&…

熔断与降级的那些事

什么熔断 熔断&#xff08;Circuit Breaker&#xff09;是一种用于限制系统请求的机制&#xff0c;其目的是防止系统在发生故障或异常情况下继续执行无效的调用&#xff0c;从而避免雪崩效应和进一步的系统损害。 熔断器通常用于分布式系统中的微服务架构中&#xff0c;它负责…

taro3 微信小程序 createIntersectionObserver 监听无效

项目&#xff1a; taro3 vue3 官方文档 版本&#xff1a;3.x Taro.createIntersectionObserver(component, options) 创建并返回一个 IntersectionObserver 对象实例。在自定义组件或包含自定义组件的页面中&#xff0c;应使用 this.createIntersectionObserver([options]) …

FSMC外设—扩展外部SRAM

目录 FSMC—扩展外部SRAM 前言 SRAM SRAM控制原理 SRAM芯片外观 SRAM芯片的内部功能框架 SRAM信号线 SRAM的存储矩阵 地址译码器、列I/O及I/O数据电路 控制电路 SRAM的读写流程 FSMC FSMC简介 FSMC框图剖析 通讯引脚 存储器控制器 时钟控制逻辑 FSMC的地址映…

【USRP X410】LabVIEW参考架构软件,用于使用Ettus USRP X410对无线系统进行原型验证

LabVIEW参考架构软件&#xff0c;用于使用Ettus USRP X410对无线系统进行原型验证 设备 1 MHz to 7.2 GHz&#xff0c;400 MHz带宽&#xff0c;GPS驯服OCXO&#xff0c;USRP软件无线电设备 - Ettus USRP X410集成硬件和软件&#xff0c;可帮助您制作高性能无线系统的原型&…

探索非洲专线物流的新时代_国际物流供应链管理平台_箱讯科技

随着全球化的发展&#xff0c;非洲作为一个充满机遇和挑战的大陆&#xff0c;吸引着越来越多的企业和投资者。然而&#xff0c;由于非洲的地理复杂性和基础设施不完善&#xff0c;物流问题一直是制约非洲发展的瓶颈之一。为了解决这一问题&#xff0c;非洲专线物流应运而生。本…

分布式数据库HBase,它到底是怎么组成的?

原文链接&#xff1a;http://www.ibearzmblog.com/#/technology/info?id3f432a2451f5f9cb9a14d6e756036b67 前言 大数据的核心问题无非就是存储和计算这两个。Hadoop中的HDFS解决了数据存储的问题&#xff0c;而HBase就是在HDFS上构建&#xff0c;因此Hbase既能解决大数据存…

【广州华锐互动】AR远程巡检系统在设备维修保养中的作用

随着科技的不断发展&#xff0c;AR(增强现实)远程巡检系统在设备检修中发挥着越来越重要的作用。这种系统可以将AR技术与远程通信技术相结合&#xff0c;实现对设备检修过程的实时监控和远程指导&#xff0c;提高设备检修的效率和质量。 首先&#xff0c;AR远程巡检系统可以帮助…

004.PADS VX2.4常用快捷键及无模命令

1.常用快捷键&#xff1a; F2 布线(Layout) F3 布线(Router) F4 切换layer F6 选中一个导线按f6选中整个网络 TAB 旋转 CtrlA select All 全选 CtrlB sheet 切换到整线sheet可以的视图状态 CtrlC copy 复制选定对象(可以是多选或选一范围): 也可以在按住Ctrl同时拖动选定对象…

微信小程序音频播放失败:TypeError: Cannot read property ‘duration‘ of undefined

报错截图 最下面这个this.setData()报错可不用理会&#xff0c;是this取值的问题 解决 需要播放和暂停功能时&#xff0c;需要把audio以及他的src放在Page外面。不能缺少 audioCtx.onPlay() 和 audioCtx.onError()两个方法&#xff0c;且需要放在play()方法之前如果在wx.crea…

解决/usr/bin/ld: cannot find -l****解决

运行程序时出现了以下错误 在这里说明一下出现/usr/bin/ld: cannot find -l****其实都是出现了类似的问题&#xff0c;只是各自的文件不同 其中****即表示函式库文件名称&#xff0c;如上例的&#xff1a;libstdc.so、libluuid.so 其命名规则是&#xff1a;lib库名(即xxx).so …

pytorch深度学习 之一 神经网络梯度下降和线性回归

张量和随机运行&#xff0c;exp函数 import torch a torch.tensor([[1,2],[3,4]]) print(a) a torch.randn(size(10,3)) print(a) b a-a[0] print(torch.exp(b)[0].numpy())输出&#xff1a; tensor([[1, 2],[3, 4]]) tensor([[-1.0165, 0.3531, -0.0852],[-0.1065, -0.5…

【HCIA】06.静态路由

路由器的作用&#xff1a;通过路由器让不同广播域实现互联互通&#xff1b;路由可以指的是路由器&#xff0c;也可以是传递的一个动词&#xff0c;或者是一个路由条目信息。 在一个典型的数据通信网络中&#xff0c;往往存在多个不同的IP网段&#xff0c;数据在不同的IP网段之…

学会写作读后感

读书不是任务 有句俗话说:“清醒时做事&#xff0c;迷茫时读书&#xff0c;独处时思考&#xff0c;烦躁时运动”。 读书 不仅让我们 跨越时间&#xff0c;空间 去感受 作者 思想的力量&#xff0c;也连接了另一个世界——认知&#xff0c;想象&#xff0c;情感&#xff0c;美…

PDF转CAD后尺寸如何保持一致?这几种方法可以尝试一下

CAD文件是可编辑的&#xff0c;可以进行修改、添加和删除&#xff0c;这使得在CAD软件中进行编辑更加容易和灵活。这意味着&#xff0c;如果需要对图纸进行修改或者添加新的元素&#xff0c;可以直接在CAD软件中进行操作&#xff0c;而不需要重新制作整个图纸。那么将PDF文件转…

BFS广度优先搜索

目录 一、BFS的概念BFS的定义BFS的搜索方式BFS的特点 二、BFS的实战应用1.走迷宫代码实现扩展 2.升级版走迷宫&#xff08;边的权值不同&#xff09;思路代码实现扩展 3.八数码代码实现 一、BFS的概念 BFS的定义 BFS&#xff08;Breadth-First Search&#xff09;广度优先搜索…

Python 和 RabbitMQ 进行消息传递和处理

一、RabbitMQ 简介 RabbitMQ 是一个开源的消息代理软件&#xff0c;它实现了高级消息队列协议&#xff08;AMQP&#xff09;标准。它的官方客户端提供了多种编程语言的接口&#xff0c;包括 Python、Java 和 Ruby 等。它支持消息的持久化、多种交换机类型、消息通知机制、灵活…

面试题更新之-什么是响应式设计?响应式设计的基本原理是什么?如何兼容低版本的IE?css实现响应式设计的方案

文章目录 什么是响应式设计&#xff1f;响应式设计的基本原理是什么如何兼容低版本的IE&#xff1f;css实现响应式设计的方案媒体查询&#xff08;Media Queries&#xff09;&#xff1a;弹性单位&#xff08;Flexible Units&#xff09;&#xff1a;Flexbox布局&#xff1a;Gr…