设计模式-观察者模式

观察者模式


文章目录

  • 观察者模式
  • 什么是观察者模式
  • 为什么要用观察者模式
  • 如何使用观察者模式
  • 总结


什么是观察者模式

  在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
  一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。不过,在实际的项目开发中,这两种对象的称呼是比较灵活的,有各种不同的叫法,比如:Subject-Observer、Publisher-Subscriber、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener。不管怎么称呼,只要应用场景符合刚刚给出的定义,都可以看作观察者模式。

为什么要用观察者模式

  观察者模式最大的作用就是解耦,为了更符合开闭原则,使用观察者模式可以将观察者和被观察者代码解耦。借助设计模式,我们利用更好的代码结构,将一大坨代码拆分成职责更单一的小类,让其满足开闭原则、高内聚松耦合等特性,以此来控制和应对代码的复杂性,提高代码的可扩展性。
  比如我们现在有这样一个需求,我们有一个可以动态记录线程池核心参数的Starter,将核心参数定时写入本地文件。代码示例如下:

// 信息采集运行器
public class ZfDtpMonitorRunner implements ApplicationRunner {
    private ZfDtpRunInfoCollectHandler zfDtpRunInfoCollectHandler;

    @Override
    public void run(ApplicationArguments args) {
        // 开启信息采集定时任务
        this.getZfDtpRunInfoCollectHandler().startCollectSchedule();
    }
}

// 信息采集处理器(执行具体的采集任务)
public class ZfDtpRunInfoCollectHandler {
	public void startCollectSchedule() {
        ZfDtpProperties.Monitor monitor = this.getZfDtpProperties().getMonitor();
        Long initialDelayMs = monitor.getInitialDelayMs();
        Long collectIntervalMs = monitor.getCollectIntervalMs();

        // 开启定时任务
        this.getCollectExecutor().scheduleWithFixedDelay(
                this::doCollect,
                initialDelayMs,
                collectIntervalMs,
                TimeUnit.MILLISECONDS);
    }

	// 采集信息
	private void doCollect() {
		// 信息采集时间
        DateTime currentDateTime = DateUtil.dateSecond();
        String collectDay = DateUtil.format(currentDateTime, ZfDtpConstants.DAY_FORMAT_STR);
        String collectTime = DateUtil.format(currentDateTime, ZfDtpConstants.DATETIME_FORMAT_STR);

        // 本次信息采集线程池运行状态集合
        List<ZfDtpRunInfo> zfDtpRunInfoList = new ArrayList<>();

        // 采集动态线程池
        List<String> dtpNames = ZfDtpFactory.listAllZfDtpNames();
        dtpNames.forEach(threadPoolName -> {
            ZfDtpExecutor zfDtpExecutor = ZfDtpFactory.getZfDtpExecutor(threadPoolName);
            ZfDtpRunInfo zfDtpRunInfo = ZfDtpConverter.convertToRunInfo(zfDtpExecutor);
            zfDtpRunInfo.setCurrentTime(collectTime);
            zfDtpRunInfoList.add(zfDtpRunInfo);
        });

        // 本次采集信息
        ZfDtpCollectTimeInfo zfDtpCollectTimeInfo = new ZfDtpCollectTimeInfo();
        zfDtpCollectTimeInfo.setCollectDay(collectDay);
        zfDtpCollectTimeInfo.setCollectTime(collectTime);
        zfDtpCollectTimeInfo.setZfDtpRunInfoList(zfDtpRunInfoList);

        // 保存线程池状态
        saveZfDtpRunInfo(collectDay, zfDtpCollectTimeInfo);
    }
	
	// 保存运行状态
	public void saveZfDtpRunInfo(String collectDay, ZfDtpCollectTimeInfo zfDtpCollectTimeInfo) {
        // 保存至文件
        String localFilePath = CommonUtil.getFilePath(this.getLocalFilePathPrefix(), collectDay);
        ArrayList<String> appendLines = new ArrayList<>();
        appendLines.add(JSONUtil.toJsonStr(zfDtpCollectTimeInfo));
        FileUtil.appendLines(appendLines, localFilePath, StandardCharsets.UTF_8);
    }
}

  上面的代码看起来似乎没什么问题,但是现在又有新的需求,需要将采集的信息保存到ES中,并且后续有规划,将采集的信息保存到时序数据库中。那我们一直频繁的修改ZfDtpRunInfoCollectHandler显然是不合适的,不符合开闭原则。那么此时我们就可以用到观察者模式,将采集信息与记录信息两个动作解耦。接下来我们介绍如何使用观察者模式。

如何使用观察者模式

在解决上面的问题之前,我们先介绍下实现观察者模式的几种方式:

  • JDK自带的Observer/Observable。这是最基本的选择,但是API较为简陋,不太方便使用。
  • Spring的ApplicationEventPublisher和ApplicationListener。这是在Spring环境下一个很好的选择,可以很方便的进行事件发布和监听。
  • Guava的EventBus。这也是一个不错的选型,Guava提供的EventBus API非常简洁易用。
  • RxJava。RxJava是一套polyglot的响应式扩展库,它不仅支持观察者模式,还提供了丰富的操作符来compose异步和事件驱动的程序。这也是当下一个优秀的选择。
  • javax.jms提供的JMS(Java消息服务)API,更广泛用于异步消息和事件处理。
  • RabbitMQ/ActiveMQ/Kafka等消息中间件产品。当应用复杂时,可以使用消息队列来实现事件驱动和解耦。
  • 自己实现一个轻量级的事件总线。实际上,像Guava EventBus等都只是对观察者模式的轻量级实现,我们也可以自己实现一个。

我们罗列了很多种实现观察者模式的方式,但是实际上的技术选型需要结合自己的项目以及实际情况来进行选择,我们这里使用Spring的ApplicationEventPublisher和ApplicationListener来解决上面的问题。代码示例如下:

public class ZfDtpRunInfoCollectRunner implements ApplicationRunner {
    private ZfDtpProperties zfDtpProperties;
    private ScheduledThreadPoolExecutor collectExecutor;

    @Override
    public void run(ApplicationArguments args) {
        ZfDtpProperties.Monitor monitor = this.getZfDtpProperties().getMonitor();
        Long initialDelayMs = monitor.getInitialDelayMs();
        Long collectIntervalMs = monitor.getCollectIntervalMs();
        // 开启定时任务
        this.getCollectExecutor().scheduleWithFixedDelay(
                this::collectDtpRunInfoAndPublishEvent,
                initialDelayMs,
                collectIntervalMs,
                TimeUnit.MILLISECONDS);
    }

    /**
     * 采集信息并发布事件
     */
    public void collectDtpRunInfoAndPublishEvent(){
        // 信息采集时间
        DateTime currentDateTime = DateUtil.dateSecond();
        String collectDay = DateUtil.format(currentDateTime, ZfDtpConstants.DAY_FORMAT_STR);
        String collectTime = DateUtil.format(currentDateTime, ZfDtpConstants.DATETIME_FORMAT_STR);

        // 本次信息采集线程池运行状态集合
        List<ZfDtpRunInfo> zfDtpRunInfoList = new ArrayList<>();

        // 采集普通线程池
        List<String> commonNames = ZfDtpFactory.listAllCtpNames();
        commonNames.forEach(threadPoolName -> {
            ExecutorWrapper executorWrapper = ZfDtpFactory.getCtpExecutor(threadPoolName);
            ZfDtpRunInfo zfDtpRunInfo = ZfDtpConverter.convertToRunInfo(executorWrapper);
            zfDtpRunInfo.setCurrentTime(collectTime);
            zfDtpRunInfoList.add(zfDtpRunInfo);
        });

        // 采集动态线程池
        List<String> dtpNames = ZfDtpFactory.listAllZfDtpNames();
        dtpNames.forEach(threadPoolName -> {
            ZfDtpExecutor zfDtpExecutor = ZfDtpFactory.getZfDtpExecutor(threadPoolName);
            ZfDtpRunInfo zfDtpRunInfo = ZfDtpConverter.convertToRunInfo(zfDtpExecutor);
            zfDtpRunInfo.setCurrentTime(collectTime);
            zfDtpRunInfoList.add(zfDtpRunInfo);
        });

        // 本次采集信息
        ZfDtpCollectTimeInfo zfDtpCollectTimeInfo = new ZfDtpCollectTimeInfo();
        zfDtpCollectTimeInfo.setCollectDay(collectDay);
        zfDtpCollectTimeInfo.setCollectTime(collectTime);
        zfDtpCollectTimeInfo.setZfDtpRunInfoList(zfDtpRunInfoList);

        // 发布事件
        ZfDtpRunInfoCollectEvent zfDtpRunInfoCollectEvent = new ZfDtpRunInfoCollectEvent(zfDtpCollectTimeInfo);
        ZfDtpApplicationContext.publishEvent(zfDtpRunInfoCollectEvent);
    }
}

// 自定义事件类
public class ZfDtpRunInfoCollectEvent extends ApplicationEvent {
    public ZfDtpRunInfoCollectEvent(ZfDtpCollectTimeInfo zfDtpCollectTimeInfo) {
        super(zfDtpCollectTimeInfo);
    }
}

// 自定义抽象类,复用代码
public abstract class ZfDtpAbstractRunInfoCollectEventListener implements ApplicationListener<ZfDtpRunInfoCollectEvent> {
    @Override
    public void onApplicationEvent(ZfDtpRunInfoCollectEvent event) {
        ZfDtpCollectTimeInfo zfDtpCollectTimeInfo = (ZfDtpCollectTimeInfo) event.getSource();
        doCollect(zfDtpCollectTimeInfo);
    }
    abstract void doCollect(ZfDtpCollectTimeInfo zfDtpCollectTimeInfo);
}


// 自定义监听器
public class ZfDtpRunInfoCollectEventLocalFileListener extends ZfDtpAbstractRunInfoCollectEventListener {
    private ZfDtpProperties zfDtpProperties;
    private String localFilePathPrefix;

    /**
     * 采集信息
     */
    @Override
    public void doCollect(ZfDtpCollectTimeInfo zfDtpCollectTimeInfo) {
        try {
            // 获取采集信息
            String collectDay = zfDtpCollectTimeInfo.getCollectDay();

            // 保存线程池状态
            String localFilePath = CommonUtil.getFilePath(this.getLocalFilePathPrefix(), collectDay);
            ArrayList<String> appendLines = new ArrayList<>();
            appendLines.add(JSONUtil.toJsonStr(zfDtpCollectTimeInfo));
            FileUtil.appendLines(appendLines, localFilePath, StandardCharsets.UTF_8);
        } catch (Exception e) {
            log.error("监听到线程池运行状态信息,保存到本地失败", e);
        }
    }
}

// 在Configuration类中将监听器注册到Spring容器中
    @Bean
    @DependsOn("zfDtpRunInfoCollectRunner")
    @ConditionalOnProperty(name = ZfDtpConstants.COLLECT_TYPE, matchIfMissing = true, havingValue = "localFile")
    public ZfDtpRunInfoCollectEventLocalFileListener zfDtpRunInfoCollectEventLocalFileListener(ZfDtpProperties zfDtpProperties) {
        ZfDtpProperties.Monitor monitorConfig = zfDtpProperties.getMonitor();
        String localFilePathPrefix = CommonUtil.getLocalFilePathPrefix(monitorConfig.getLocalFilePath());
        return new ZfDtpRunInfoCollectEventLocalFileListener(zfDtpProperties, localFilePathPrefix);
    }

这样,我们就完成了整个观察者模式。

总结

优点:

    1. 解耦:观察者模式可以解耦观察者和被观察者之间的约束关系。观察者不需要知道有多少观察者对象以及它们的细节。
    1. 增加灵活性:可以在程序运行时增加观察者,也可以删除观察者。
    1. 支持广播通信:被观察者可以向多个观察者对象广播通知。

缺点:

    1. 过度通知:如果观察者过多,被观察者的一个变化会引起大量观察者对象的更新,可能会产生过度通知的问题。
    1. 时序问题:当一个观察者触发另一个观察者的更新时,会产生时序问题。
    1. 增加系统复杂性:观察者模式会引入较多的抽象对象和关系,增加系统的复杂性。

  对于第一点缺点,可以采用推送模型和拉取模型进行优化。对于第二点缺点,可以对观察者进行分类,分级触发。

  总体来说,观察者模式的优点远远大于缺点。它实现了低耦合,高内聚的设计原则,有效地解决了主题对象与观察者对象之间的通信问题,提高了系统的灵活性和扩展性。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DX算法还原

早在之前作者就写过一篇关于顶象的滑块验证&#xff0c;潦潦草草几句话就带过了。 出于互相学习的想法&#xff0c;给了一个大学生&#xff0c;奈何不讲武德把源码甩群里了&#xff0c;虽然在大佬们眼里不难&#xff0c; 不过拿着别人的东西乱传还是不太好。自认倒霉&#xf…

【ONE·C++ || 二叉搜索树】

总言 二叉树进阶&#xff1a;主要介绍二叉搜索树相关内容。 文章目录 总言1、基本介绍1.1、什么是二叉搜索树 2、相关实现2.1、基本框架2.1.1、如何构建二叉树单节点2.1.2、如何定义一个二叉搜索树 2.2、非递归实现&#xff1a;插入、查找、删除2.2.1、二叉搜索树插入&#xf…

Windows 程序开机自启动速度优化,为什么腾讯会议自启动速度那么高?

目录 一、问题的说明和定义 二、问题的分析 1.问题初步分析 2.详细的分析&#xff1a; 2.1Windows常见的自启动方式 2.2Windows常见的自启动方式的细节分析 三、问题的解决方案 1、为什么腾讯会议Rooms那么快 2.我们是否可以跟腾讯会议一样快 一、问题的说明和定义 这…

5. 操作系统基础

5. 操作系统基础 常考面试题 说说你对进程的理解⭐⭐⭐ 程序是指令、数据及其组织形式的描述,而进程则是程序的运行实例,包括程序计数器、寄存器和变量的当前值。 Linux的进程结构,一般分为三部分:代码段、数据段(.data与.bss)和堆栈段。 代码段用于存放程序代码,如果有…

武忠祥老师每日一题||不定积分基础训练(六)

解法一&#xff1a; 求出 f ( x ) , 进而对 f ( x ) 进行积分。 求出f(x),进而对f(x)进行积分。 求出f(x),进而对f(x)进行积分。 令 ln ⁡ x t , 原式 f ( t ) ln ⁡ ( 1 e t ) e t 令\ln xt,原式f(t)\frac{\ln (1e^t)}{e^t} 令lnxt,原式f(t)etln(1et)​ 则 ∫ f ( x ) d…

java学习之枚举二

目录 一、enum关键字实现枚举 二、注意事项 一、对Season2进行反编译&#xff08;javap&#xff09; ​编辑 三、练习题 第一题 第二题 一、enum关键字实现枚举 package enum_;public class Enumeration03 {public static void main(String[] args) {System.out.println…

Python每日一练(20230506) 存在重复元素I、II、III

目录 1. 存在重复元素 Contains Duplicate I 2. 存在重复元素 Contains Duplicate II 3. 存在重复元素 Contains Duplicate III &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 1. 存在重…

【Linux 裸机篇(八)】I.MX6U EPIT 定时器中断、定时器按键消抖

目录 一、EPIT 定时器简介二、定时器按键消抖 一、EPIT 定时器简介 EPIT 的全称是&#xff1a; Enhanced Periodic Interrupt Timer&#xff0c;直译过来就是增强的周期中断定时器&#xff0c;它主要是完成周期性中断定时的。学过 STM32 的话应该知道&#xff0c; STM32 里面的…

电脑系统怎么选?Win?MacOS?Linux?

马上要学编程了&#xff0c;我们要学什么操作系统呢&#xff1f;是MacOS&#xff0c;还是Windows&#xff0c;或者是Linux或者其他&#xff01;那我们今天就来说说MacOS系统和Windows系统的优缺点&#xff0c;也介绍一下其他的系统。让你心里有底&#xff01; 1、Windows 首先当…

Neo4j导出和导入数据库

Neo4j 4.x版本和5.x版本的导出导入有区别&#xff0c;这里分开来讲。 1 4.x版本 1.1 准备 导入导出之前要先关闭neo4j服务。 .neo4j stop 1.2 数据导出 进入$NEO4J_HOME%/bin目录执行如下数据库导出命令&#xff1a; neo4j-admin dump --databaseneo4j --toF:/neo4j_bac…

《Netty》从零开始学netty源码(五十四)之PoolThreadLocalCache

PoolThreadLocalCache 前面讲到PoolThreadCache&#xff0c;它为线程提供内存缓存&#xff0c;当线程需要分配内存时可快速从其中获取&#xff0c;在Netty中用PoolThreadLocalCache来管理PoolThreadCache&#xff0c;它的数据结构如下&#xff1a; PoolThreadLocalCache相当…

Unity3D:内置着色器的用途和性能

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 3D工具集&#xff1a; NSDT简石数字孪生 内置着色器的用途和性能 Unity 中的着色器是通过__材质__来使用的&#xff0c;材质本质上结合了着色器代码与纹理等参数。此处提供了关于着色器/材质关系的深入说明。 当选择材质…

延时队列的三种实现方案

延时队列的三种实现方案 什么是延时队列延时队列的应用场景基于Java DelayQueue的实现源码剖析 基于Redis的zset实现实现步骤Redis延时队列优势Redis延时队列劣势 基于RabbitMQ的延时队列实现TTL DXL(死信队列)插件实现 总结参考文章 什么是延时队列 在分布式系统中&#xff…

Java之多线程初阶2

目录 一.上节内容复习 1.进程和线程的区别 2.创建线程的四种方式 二.多线程的优点的代码展示 1.多线程的优点 2.代码实现 三.Thread类常用的方法 1.Thread类中的构造方法 2.Thread类中的属性 1.为线程命名并获取线程的名字 2.演示isDaemon() 3.演示isAlive() 4.演示…

ChatGPT写文章效果-ChatGPT写文章原创

ChatGPT写作程序&#xff1a;让文案创作更轻松 在当前数字化的时代&#xff0c;营销推广离不开文案创作。然而&#xff0c;写作对许多人来说可能是一项耗时而枯燥的任务。如果您曾经为写出较高质量的文案而苦恼过&#xff0c;那么ChatGPT写作程序正是为您而设计的。 ChatGPT是…

Python 模块

目录 1.模块导入语言 1.1 import 语句 1.2 from…import 语句​编辑 2. 搜索路径 3.命名空间和作用域 4.globals() 和 locals() 函数 5.reload() 函数 6.Python中的包 7.自定义模块及其调用 7.1 创建模块及__init__.py初始化文件 7.2 __init__.py的参数__all__ …

【vite+vue3.2 项目性能优化实战】打包体积分析插件rollup-plugin-visualizer视图分析

rollup-plugin-visualizer是一个用于Rollup构建工具的插件&#xff0c;它可以生成可视化的构建报告&#xff0c;帮助开发者更好地了解构建过程中的文件大小、依赖关系等信息。 使用rollup-plugin-visualizer插件&#xff0c;可以在构建完成后生成一个交互式的HTML报告&#xf…

从血缘进化论的角度,破解婆媳关系的世纪难题

从血缘进化论的角度&#xff0c;破解婆媳关系的世纪难题 有个粉丝的留言&#xff0c;很长很复杂&#xff0c;是关于他们家的婆媳关系问题。 青木老师&#xff0c;您好&#xff0c;我也有一些问题想咨询您&#xff0c;是关于婆媳关系的&#xff0c;字数有些多&#xff0c;分开…

【ElasticSearch】EQL操作相关

文章目录 EQL操作基础语法数据准备数据窗口搜索统计符合条件的事件事件序列 安全检测数据准备查看数据导入情况获取 regsvr32 事件的计数检查命令行参数检查恶意脚本加载检查攻击成功可能性 EQL操作 EQL 的全名是 Event Query Language (EQL)。事件查询语言&#xff08;EQL&…

【问题记录】flask开发blog

文章目录 小知识点问题1. 文章标签显示错误2. 文章状态无法回显&#xff08;open)3. 用户管理页面&#xff0c;图标无法显示4. BuildError5. 用户管理添加用户&#xff0c;使用重复的用户名会报错(open)6. 添加用户&#xff0c;不上传头像会报错(open)7. 部分标签删除时报错&am…