观察者模式:解耦对象间的依赖关系

news2025/4/1 19:06:56

观察者模式:解耦对象间的依赖关系

JDK 中曾直接提供对观察者模式的支持,但因其设计局限性,现已被标记为“过时”(Deprecated)。不过,观察者模式的思想在 JDK 的事件处理、spring框架等仍有广泛应用。下面我将从实际的问题出发,带你详细了解观察者设计模式。

意图

观察者模式(Observer Pattern)定义对象间的一对多依赖关系,当一个对象(被观察者/主题)状态发生改变时,所有依赖它的对象(观察者)都会自动收到通知并更新。其核心目的是解耦主题与观察者,使二者能够独立变化而不互相影响。

GoF定义:定义一种订阅机制,在对象事件发生时通知多个“观察”该对象的其他对象。


问题

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

顾客可以每天来商店看看产品是否到货。 但如果商品尚未到货时, 绝大多数来到商店的顾客都会空手而归。

前往商店和发送垃圾邮件

另一方面, 每次新产品到货时, 商店可以向所有顾客发送邮件 (可能会被视为垃圾邮件)。 这样, 部分顾客就无需反复前往商店了, 但也可能会惹恼对新产品没有兴趣的其他顾客。

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

解决方案

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

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

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

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

实际应用中可能会有十几个不同的订阅者类跟踪着同一个发布者类的事件, 你不会希望发布者与所有这些类相耦合的。 此外如果他人会使用发布者类, 那么你甚至可能会对其中的一些类一无所知。

因此, 所有订阅者都必须实现同样的接口, 发布者仅通过该接口与订阅者交互。 接口中必须声明通知方法及其参数, 这样发布者在发出通知时还能传递一些上下文数据。

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


现实场景类比

微信公众号与订阅用户

  • 主题(Subject):微信公众号。
  • 观察者(Observer):订阅用户。
  • 过程:用户订阅公众号后,公众号发布新文章时,所有订阅用户自动收到推送。公众号无需知道用户是谁,用户也可以随时取消订阅。

观察者模式结构

  • Subject:定义注册、注销、通知观察者的接口。
  • ConcreteSubject:维护观察者列表,存储状态,状态变化时触发通知。
  • Observer:定义update()方法。
  • ConcreteObserver:实现update()逻辑(如更新UI、执行业务)。

Java代码示例

// 主题接口
interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObservers();
}

// 具体主题:天气预报站
class WeatherStation implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private float temperature;

    public void setTemperature(float temp) {
        this.temperature = temp;
        notifyObservers();
    }

    @Override
    public void registerObserver(Observer o) { observers.add(o); }
    @Override
    public void removeObserver(Observer o) { observers.remove(o); }
    @Override
    public void notifyObservers() {
        for (Observer o : observers) {
            o.update(temperature);
        }
    }
}

// 观察者接口
interface Observer {
    void update(float temperature);
}

// 具体观察者:手机天气App
class PhoneApp implements Observer {
    @Override
    public void update(float temperature) {
        System.out.println("手机App收到温度更新:" + temperature + "℃");
    }
}

// 使用
public class Client {
    public static void main(String[] args) {
        WeatherStation station = new WeatherStation();
        PhoneApp app = new PhoneApp();
        station.registerObserver(app);
        station.setTemperature(25.5f); // 触发通知
    }
}

适合场景

  • 事件驱动系统:如GUI按钮点击事件、消息队列。
  • 跨层通知:业务逻辑层状态变化需同步更新UI层。
  • 广播通信:如聊天室消息群发、微服务配置中心更新。
  • 需要动态订阅/取消订阅的场景。
  • 解耦: 当一个对象变更时,需要跨模块(跨类)通知其他模块(类),非常适合观察者模式

实现方式:实战步骤

  1. 仔细检查你的业务逻辑, 试着将其拆分为两个部分: 独立于其他代码的核心功能将作为发布者; 其他代码则将转化为一组订阅类。

  2. 声明订阅者接口。 该接口至少应声明一个 update方法。

  3. 声明发布者接口并定义一些接口来在列表中添加和删除订阅对象。 记住发布者必须仅通过订阅者接口与它们进行交互。

  4. 确定存放实际订阅列表的位置并实现订阅方法。 通常所有类型的发布者代码看上去都一样, 因此将列表放置在直接扩展自发布者接口的抽象类中是显而易见的。 具体发布者会扩展该类从而继承所有的订阅行为。

但是, 如果你需要在现有的类层次结构中应用该模式, 则可以考虑使用组合的方式: 将订阅逻辑放入一个独立的对象, 然后让所有实际订阅者使用该对象。

  1. 创建具体发布者类。 每次发布者发生了重要事件时都必须通知所有的订阅者。

  2. 在具体订阅者类中实现通知更新的方法。 绝大部分订阅者需要一些与事件相关的上下文数据。 这些数据可作为通知方法的参数来传递。

但还有另一种选择。 订阅者接收到通知后直接从通知中获取所有数据。 在这种情况下, 发布者必须通过更新方法将自身传递出去。 另一种不太灵活的方式是通过构造函数将发布者与订阅者永久性地连接起来。

  1. 客户端必须生成所需的全部订阅者, 并在相应的发布者处完成注册工作。

优缺点

优点

  • 符合开闭原则:新增观察者无需修改主题。
  • 松耦合:主题和观察者仅依赖抽象接口。
  • 支持动态订阅机制。

缺点

  • 观察者更新顺序不可控,可能引发意外问题。
  • 频繁通知可能影响性能(需优化为批量或异步通知)。
  • 调试困难:多个观察者的调用链路复杂。

和其他设计模式的关系

  • 发布-订阅模式:观察者模式的升级版,通过事件通道解耦生产者和消费者。
  • 中介者模式:通过中介者协调多个对象交互,而观察者直接通信。
  • 责任链模式:通过链式传递请求,观察者模式是“一对多”的广播机制。

观察者模式在spring中的应用

spring中的ApplicationListener就是典型的观察者模式的应用,其中各个角色对应关系如下:

组件/角色对应观察者模式中的角色作用
ApplicationEvent事件对象(状态/消息载体)封装事件数据(如用户注册事件、订单创建事件)。
ApplicationListener观察者(Observer)监听特定事件并定义响应逻辑(如发送邮件、记录日志)。
ApplicationEventPublisher主题(Subject)负责发布事件,通知所有监听该事件的观察者。

总结

观察者模式是构建松耦合、事件驱动系统的核心工具,广泛应用于GUI框架、消息中间件等场景。在实现时需权衡实时性与性能,并谨慎处理观察者的生命周期,避免内存泄漏。


如果文章对你有帮助,给个免费的赞鼓励一下吧!

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

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

相关文章

windows第二十章 单文档应用程序

文章目录 单文档定义新建一个单文档应用程序单文档应用程序组成&#xff1a;APP应用程序类框架类&#xff08;窗口类&#xff09;视图类&#xff08;窗口类&#xff0c;属于框架的子窗口&#xff09;文档类&#xff08;对数据进行保存读取操作&#xff09; 直接用向导创建单文档…

通信协议之串口

文章目录 简介电平标准串口参数及时序USART与UART过程引脚配置 简介 点对点&#xff0c;只能两设备通信只需单向的数据传输时&#xff0c;可以只接一根通信线当电平标准不一致时&#xff0c;需要加电平转换芯片&#xff08;一般从控制器出来的是信号是TTL电平&#xff09;地位…

Java入门知识总结——章节(二)

ps&#xff1a;本章主要讲数组、二维数组、变量 一、数组 数组是一个数据容器&#xff0c;可用来存储一批同类型的数据 &#x1f511;&#xff1a;注意 类也可以是一个类的数组 public class Main {public static class Student {String name;int age; // 移除 unsignedint…

Verilog 中寄存器类型(reg)与线网类型(wire)的区别

目录 一、前言 二、基本概念与分类 1.寄存器类型 2.线网类型 三、六大核心区别对比 四、使用场景深度解析 1.寄存器类型的典型应用 2. 线网类型的典型应用 五、常见误区与注意事项 1. 寄存器≠物理寄存器 2.未初始化值陷阱 3.SystemVerilog的改进 六、总结 …

【Linux加餐-验证UDP:TCP】-windows作为client访问Linux

一、验证UDP-windows作为client访问Linux UDP client样例代码 #include <iostream> #include <cstdio> #include <thread> #include <string> #include <cstdlib> #include <WinSock2.h> #include <Windows.h>#pragma warning(dis…

Rust vs. Go: 性能测试(2025)

本内容是对知名性能评测博主 Anton Putra Rust vs. Go (Golang): Performance 2025 内容的翻译与整理, 有适当删减, 相关数据和结论以原作结论为准。 再次对比 Rust 和 Go&#xff0c;但这次我们使用的是最具性能优势的 HTTP 服务器库---Hyper&#xff0c;它基于 Tokio 异步运…

JDBC的详细使用

1. JDBC概述 JDBC[Java Database Connectivity]是 Java 语言中用于连接和操作数据库的一套标准 API。它允许 Java 程序通过统一的方式与各种关系型数据库&#xff0c;如 MySQL、Oracle、SQL Server 等交互&#xff0c;执行 SQL 语句并处理结果。 1.1 JDBC原理 JDBC的核心原理…

瑞芯微 RKrga接口 wrapbuffer_virtualaddr 使用笔记

一、源码 官方在librga中给了很多 demo 以供参考&#xff0c;例如 imresize 操作&#xff1a; /** Copyright (C) 2022 Rockchip Electronics Co., Ltd.* Authors:* YuQiaowei <cerf.yurock-chips.com>** Licensed under the Apache License, Version 2.0 (the &qu…

【数据结构】[特殊字符] 并查集优化全解:从链式退化到近O(1)的性能飞跃 | 路径压缩与合并策略深度实战

并查集的优化 导读一、合并优化1.1 基本原理1.2 按大小合并1.3 按秩合并1.4 两种合并的区别**1.4.1 核心目标****1.4.2 数据存储****1.4.3 合并逻辑****1.4.4 树高控制****1.4.5 适用场景****1.4.6 路径压缩兼容性****1.4.7 极端案例对比****1.4.8 小结**二、查找优化2.1 路径压…

如何在 AI 搜索引擎(GEO)霸屏曝光,快速提升知名度?

虽然大多数人仍然使用 Google 来寻找答案&#xff0c;但正在发生快速转变。ChatGPT、Copilot、Perplexity 和 DeepSeek 等 LLM 已成为主流。这主要是因为每个都有自己的免费和公共版本&#xff0c;并且总是有重大的质量改进。 许多人每天都使用这些工具来提问和搜索互联网&…

VLAN综合实验二

一.实验拓扑&#xff1a; 二.实验需求&#xff1a; 1.内网Ip地址使用172.16.0.0/分配 2.sw1和SW2之间互为备份 3.VRRP/STP/VLAN/Eth-trunk均使用 4.所有Pc均通过DHCP获取IP地址 5.ISP只能配置IP地址 6.所有…

Kubernetes》k8s》Containerd 、ctr 、cri、crictl

containerd ctr crictl ctr 是 containerd 的一个客户端工具。 crictl 是 CRI 兼容的容器运行时命令行接口&#xff0c;可以使用它来检查和调试 k8s 节点上的容器运行时和应用程序。 ctr -v 输出的是 containerd 的版本&#xff0c; crictl -v 输出的是当前 k8s 的版本&#x…

SQL语句及其应用(中)(DQL语句之单表查询)

SQL语句的定义: 概述: 全称叫 Structured Query Language, 结构化查询语言, 主要是实现 用户(程序员) 和 数据库软件(例如: MySQL, Oracle)之间交互用的. 分类: DDL: 数据定义语言, 主要是操作 数据库, 数据表, 字段, 进行: 增删改查(CURD) 涉及到的关键字: create, drop, …

算法题(111):k与迷宫

审题&#xff1a; 本题需要我们寻找迷宫中的所有出口&#xff0c;若有出口需要输出距离最近的出口的距离&#xff0c;若没有就输出-1 时间复杂度&#xff1a;由于边距为1&#xff0c;我们本题采用bfs算法&#xff0c;在最坏的情况下我们需要遍历所有位置&#xff0c;时间复杂度…

Redis-常用命令

目录 1、Redis数据结构 2、命令简介 2.1、通用命令 DEL EXISTS EXPIRE 2.2、String命令 SET和GET MSET和MGET INCR和INCRBY和DECY SETNX SETEX 2.3、Key的层级结构 2.4、Hash命令 HSET和HGET HMSET和HMGET HGETALL HKEYS和HVALS HINCRBY HSETNX 2.5、List命…

从虚拟现实到可持续设计:唐婉歆的多维创新之旅

随着线上线下融合逐渐成为全球家居与建材行业的发展趋势,全球市场对高品质、个性化家居和建材产品的需求稳步攀升,也对设计师提出更高的要求。在这一背景下,设计师唐婉歆将以产品设计师的身份,正式加入跨国企业AmCan 美加集团,投身于备受行业瞩目的系列设计项目。她将负责Showr…

音视频 四 看书的笔记 MediaPlayerService

Binder机制看这里 Binde机智 这是一个分割符 Binder机智 分割(goutou) Binder机制 MediaPlayerService多媒体框架中一个非常重要的服务。MediaPlayerService 我原称之为链接之王 图片来源 MediaPlayer 是客户端 C/S 中的CMediaPlayerService MediaPlayerService::Client 是服…

vmware 创建win10 系统,虚拟机NAT网络设置

虚拟机设置&#xff1a; 物理机本机创建桥接&#xff1a; 如何创建桥接&#xff0c;请自行脑补~

【初阶数据结构】线性表之双链表

文章目录 目录 一、双链表的概念 二、双链表的实现 1.初始化 2.尾插 3.头插 4.打印 5.判断双链表是否为空 6.尾删 7.头删 8.查找 9.在指定的位置之后插入数据 10.删除指定位置的数据 11.销毁 三、完整源码 总结 一、双链表的概念 链表的结构非常多样&#xff0…

智能路由系统-信息泄露漏洞挖掘

1.漏洞描述&#xff1a; Secnet-智能路由系统 actpt_5g.data 信息泄露&#xff0c;攻击者可利用此漏洞收集敏感信息&#xff0c;从而为下一步攻击做准备。 2.fofa搜索语句 title"安网-智能路由系统" || title"智能路由系统" || title"安网科技-智能…