当前位置: 首页 > news >正文

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

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

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.dtcms.com/a/98326.html

相关文章:

  • 【嵌入式学习3】多任务编程
  • (二)万字长文解析:deepResearch如何用更长的思考时间换取更高质量的回复?各家产品对比深度详解
  • 锐评|希捷NVMe闪存+磁盘混合存储阵列
  • AB包介绍及导出工具实现+AB包资源简单加载
  • Flutter和React Native在开发app中,哪个对java开发工程师更适合
  • 如何看待职场中的“向上管理”
  • c中的变量命名规则
  • 【精修版】【中项】系统集成项目管理工程师:第12章 项目进度管理-12.4估算活动持续时间
  • Python爬虫:开启数据抓取的奇幻之旅(一)
  • 点云库(Point Cloud Library, PCL)
  • 蓝桥复习 1(Init)
  • TCP网络编程与多进程并发实践
  • STM32_HAL开发环境搭建【Keil(MDK-ARM)、STM32F1xx_DFP、 ST-Link、STM32CubeMX】
  • buildroot(1) -- 编译过程记录
  • [ 工具使用指南 ] | Visual Studio 2019 调试
  • 计算机底层基石:原码、反码、补码、移码深度剖析
  • HTML5 Web 存储学习笔记
  • YOLOv8+ Deepsort+Pyqt5车速检测系统
  • [ CTFshow ] Java web279-web281
  • LangChain 基础系列之 Prompt 工程详解:从设计原理到实战模板
  • AI日报 - 2025年3月30日
  • Vue3组合式API与选项式API的核心区别与适用场景
  • JDBC、excute()、DriveManager、Connection、Statement、自建JDBC工具类、占位符
  • GO语言开篇
  • Rust基础语法
  • c++-函数增强
  • JVM介绍
  • 《DeepSeek+Office办公套件使用课程》
  • MSTP+VRRP三层架构综合实验
  • ABAP FPM