Java中的设计模式(一):观察者模式

简介: 让我们从司机和红绿灯的关系来探究一下观察者模式

Java中的设计模式(一):观察者模式.png

人生苦短,不如养狗

一、从“红灯停,绿灯行”开始

  在汽车界,不论你是迅捷如风的秋名山车神,还是新上岗的马路杀手,在交通灯前都需要遵守这样一条铁律——“红灯停,绿灯行”。当你坐上驾驶位的那一刻,就注定了你必须随“灯”而行。

  在上面的场景中出现了两个角色—— 交通灯驾驶员 ,驾驶员需要观察交通灯的变色情况(即 变红变绿 ),根据不同的变色情况作出对应的行驶措施(即 )。这一对象间的行为模式在软件设计中同样存在,也就是我们下面要学习的设计模式—— 观察者模式

二、基本概念

1. 定义

  观察者模式 (Observer Pattern)是用于建立一种对象和对象之间依赖关系的 对象行为型设计模式 ,其定义为:

在对象之间定义一个一对多的依赖,当一个对象状态改变时,所有依赖的对象都会自动收到通知。

  在这一定义中明确了两个对象:

  • 目标对象:即被依赖的对象或被观察的对象,当状态发生变更时会通知所有的观察者对象。在上面的例子中,交通灯就是被观察的对象;
  • 观察者对象:即依赖的对象,当观察的对象状态发生变更时会自动收到通知,根据收到的通知作出相应的行为(或进行对应状态的更新操作)。在上面的例子中,驾驶员就是其中的观察者;

  其结构图如下:

观察者模式.jpg

  除此以外,观察者模式 也被称为 发布订阅模式(Publish-Subscribe Pattern)、 模型-视图模式 (Model-View Pattern)、 源-监听器模式 (Source-Listener Pattern)等等。

2. 基于观察者模式的事件驱动模型

  在实际的编程过程中,我们更多的是关注某一事件的发生,比如上面所说的 交通灯变红/变绿 这样一个事件,而在发生了交通灯变色之后,汽车才会做出相应的举措 (停车/启动) ,这就是 事件驱动模型 ,也称委派事件模型(Delegation Event Model,DEM)。在事件驱动模型中有以下三个要素:

  • 事件源:即最初发生事件的对象,也对应者观察者模式中被观察的目标对象;
  • 事件对象:即被触发的事件,事件对象需要有能够执行该事件的主体,即事件源;
  • 事件监听者:即监听发生事件的对象,当监听的对应对象发生某个事件之后,事件监听者会根据发生的事件做出预先设定好的相应举措;

  上述所说的事件驱动模型其实是通过观察者模式来实现的,下面是观察者模式和事件驱动模型的对应关系:

基于观察者的事件驱动模型.jpg

  从上图中可以看到,在事件驱动模型中,事件监听者就对应着观察者模式中的观察者对象,事件源和事件共同组成了被观察和被处理的目标对象,其中事件源对应着被观察的目标对象(即事件监听者会被注册到事件源上),而发生在事件源上的事件则是需要被事件监听者处理的对象。

  发生在事件源上的事件实际上是对观察者模式中的目标对象的状态变更这一动作的扩展,单一的状态变更无法更好的满足开发的需要,而事件则具备更好的扩展性。

三、源码探究

1. JDK中的观察者模式

  观察者模式是如此的常用,以至于JDK从1.0版本开始就提供了对该模式的支持。在JDK中提供了 Observable 类和 Observer 接口,前者提供了被观察对象的基类实现,后者则提供了观察者的通用处理接口。通过 继承/实现 这两个类,开发可以很轻松的完成观察者模式的使用。

  下面具体分析一下 Obserable 类中的 notifyObservers(Object arg) 方法:

publicvoidnotifyObservers(Objectarg) {
// 局部变量,用于存放观察者集合Object[] arrLocal;
// 这里对目标对象加锁,防止获取目标对象状态和观察者集合时出现线程安全问题。// 但是在通知观察者进行相应处理时则不需要保障线程安全。// 在当前竞争的情况下,最坏的结果如下:// 1) 一个新加入的观察者会错过本地通知;// 2) 一个最近被注销的观察者会被错误地通知synchronized (this) {
// 判断当前目标对象状态是否变更if (!changed)
return;
arrLocal=obs.toArray();
// 清除状态clearChanged();
        }
for (inti=arrLocal.length-1; i>=0; i--)
// 通知所有观察者进行对应操作            ((Observer)arrLocal[i]).update(this, arg);
 }

  从该方法中可以看到想要完成对所有观察者的通知需要满足 目标对象状态改变 这一必要条件。为了保证获取状态和观察者集合时线程安全,这里使用了 synchronized 关键字和局部变量。但是同步代码块并没有包含调用观察者 update 方法,这就导致了可能会出现有观察者没有收到通知或者收到错误的通知。

  对于JDK提供的观察者模式,使用的流程为: Observable.setChanged() -> Observable.notifyObservers(Object arg)

2. JDK中的事件驱动模型

  除了观察者模式,JDK还实现了对事件驱动模型的支持。为此,JDK提供了 EventObject 类 和 EventListener 接口来支持这一模型。前者代表了事件驱动模型中的 事件对象 ,后者则代表了 事件监听者

  首先我们来看下 EventObject 的构造函数:

publicEventObject(Objectsource) {
if (source==null)
thrownewIllegalArgumentException("null source");
this.source=source;
 }

  可以看到,在构造函数中必须传入一个 source 对象,该对象在官方注释中被定义为最初发生事件的对象。这个解释乍一看还是有点抽象,结合上面交通灯的例子可能会更好理解一点。

  在交通灯的例子中,交通灯就是 事件源 ,而交通灯变色就是 事件 ,司机就是事件监听者。司机作为事件监听者实际观察的对象是交通灯,当发生交通灯变色事件之后,司机会根据交通灯变色事件进行相应的处理(也就是进行事件的处理)。

  根据上面的逻辑我们不难看到,司机这一事件监听者实际上是注册到交通灯这一事件源上,然后去处理交通灯所发生的事件。这里我们可以看下JDK提供的事件监听者接口 EventListener ,可以看到这里只是声明了一个接口,里面没有任何的方法。从个人角度来理解,这可能是作者考虑到众口难调的情况,与其费尽周折想一个通用的方法,不如单纯定义一个接口,让使用者自由发挥。

2. Spring中的事件驱动模型--发布/订阅模式

  Spring框架对于事件驱动模型做了数据模型上的进一步明确,在原有的概念上又新增了 事件发布者 的角色,由此得到了一个新的模式——发布/订阅模式。

  在JDK的基础上,Spring框架提供了  ApplicationEvent 、  ApplicationListener 和  ApplicationEventPublisher 三个基础类来支持发布/订阅模式。其中 ApplicationEventApplicationListener 分别继承了 EventObjectEventListener ,其作用也和这两个类相同,就不再过多赘述。这里具体关注一下 ApplicationEventPublisher 这个新引入的类,这个新引入的类就对应着上面事件驱动模型中事件源这一角色,区别于JDK中的自由奔放,这里将事件源定义为了事件发布者,并提供了一下两个方法:

@FunctionalInterfacepublicinterfaceApplicationEventPublisher {
/*** 通知所有注册到发布者上面的监听器进行对应的事件处理** @param event 用于发布的事件,这里的事件对象必须是ApplicationEvent的基类*/defaultvoidpublishEvent(ApplicationEventevent) {
publishEvent((Object) event);
  }
/*** 通知所有注册到发布者上面的监听器进行对应的事件处理* * @param event 用于发布的事件,任意类型事件都可以进行处理*/voidpublishEvent(Objectevent);
}

  可以看到为了保证扩展性和自由行,Spring即提供了基于 ApplicationEvent 类型的事件发布方法,也提供了 Object 类型的事件处理。这里我们选取 AbstractApplicationContext 这一 ApplicationEvent 的基类来一窥Spring中事件发布的逻辑:

@OverridepublicvoidpublishEvent(ApplicationEventevent) {
publishEvent(event, null);
  }
protectedvoidpublishEvent(Objectevent, @NullableResolvableTypeeventType) {
Assert.notNull(event, "Event must not be null");
// 将事件包装成ApplicationEventApplicationEventapplicationEvent;
if (eventinstanceofApplicationEvent) {
applicationEvent= (ApplicationEvent) event;
    } else {
applicationEvent=newPayloadApplicationEvent<>(this, event);
if (eventType==null) {
eventType= ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
      }
    }
// 如果可能,现在立即进行多播// 或一旦初始化多播器就懒惰地进行多播if (this.earlyApplicationEvents!=null) {
this.earlyApplicationEvents.add(applicationEvent);
    } else {
// 进行事件的广播,这里是进行广播的关键getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
    }
// 通过父类的context进行事件发布if (this.parent!=null) {
if (this.parentinstanceofAbstractApplicationContext) {
        ((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
      }
else {
this.parent.publishEvent(event);
      }
    }
  }
/*** 将事件广播给对应的监听者*/publicvoidmulticastEvent(finalApplicationEventevent, @NullableResolvableTypeeventType) {
ResolvableTypetype= (eventType!=null?eventType : resolveDefaultEventType(event));
Executorexecutor=getTaskExecutor();
for (ApplicationListener<?>listener : getApplicationListeners(event, type)) {
if (executor!=null) {
executor.execute(() ->invokeListener(listener, event));
      }
else {
invokeListener(listener, event);
      }
    }
  }

  除了事件准备的过程,进行事件广播通知给对应的监听者,然后调用监听者对应的方法,这一过程和上面看到过的 Observable 通知监听器的方法基本相同。但是区别于JDK中的同步处理,Spring中的事件处理如果存在线程池的话,还使用了线程池就行异步处理对应的事件,进一步将发布者和监听者做了解耦。

四、总结

  观察者模式最大的特定是建立了一个一对多且松散的耦合关系,观察目标只需要维持一个抽象观察者集合,无须感知具体的观察者有哪些。这样一个松散的耦合关系有利于观察目标和观察者各自进行对应的抽象处理,很好的体现了开闭原则。

  当然,观察者模式也有其弊端,比如只定义了一对多的关系,无法处理多对多的场景;又比如只能感知观察目标发生了变化,但是具体如何变化却无法了解到,等等。这些都是观察者模式无法处理的场景或存在的问题。

相关文章
|
20天前
|
设计模式 Java 开发者
设计模式揭秘:Java世界的七大奇迹
【4月更文挑战第7天】探索Java设计模式:单例、工厂方法、抽象工厂、建造者、原型、适配器和观察者,助你构建健壮、灵活的软件系统。了解这些模式如何提升代码复用、可维护性,以及在特定场景下的应用,如资源管理、接口兼容和事件监听。掌握设计模式,但也需根据实际情况权衡,打造高效、优雅的软件解决方案。
|
21天前
|
设计模式 存储 Java
23种设计模式,享元模式的概念优缺点以及JAVA代码举例
【4月更文挑战第6天】享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享技术有效地支持大量细粒度对象的重用。这个模式在处理大量对象时非常有用,特别是当这些对象中的许多实例实际上可以共享相同的状态时,从而可以减少内存占用,提高程序效率
35 4
|
21天前
|
设计模式 Java 中间件
23种设计模式,适配器模式的概念优缺点以及JAVA代码举例
【4月更文挑战第6天】适配器模式(Adapter Pattern)是一种结构型设计模式,它的主要目标是让原本由于接口不匹配而不能一起工作的类可以一起工作。适配器模式主要有两种形式:类适配器和对象适配器。类适配器模式通过继承来实现适配,而对象适配器模式则通过组合来实现
31 4
|
25天前
|
设计模式 Java 数据库
Java设计模式精讲:让代码更优雅、更可维护
【4月更文挑战第2天】**设计模式是解决软件设计问题的成熟方案,分为创建型、结构型和行为型。Java中的单例模式确保类仅有一个实例,工厂方法模式让子类决定实例化哪个类。适配器模式则协调不兼容接口间的合作。观察者模式实现了一对多依赖,状态变化时自动通知相关对象。学习和适当应用设计模式能提升代码质量和可维护性,但需避免过度使用。设计模式的掌握源于实践与不断学习。**
Java设计模式精讲:让代码更优雅、更可维护
|
20天前
|
设计模式 监控 Java
设计模式 - 观察者模式(Observer):Java中的战术与策略
【4月更文挑战第7天】观察者模式是构建可维护、可扩展系统的关键,它在Java中通过`Observable`和`Observer`实现对象间一对多的依赖关系,常用于事件处理、数据绑定和同步。该模式支持事件驱动架构、数据同步和实时系统,但需注意避免循环依赖、控制通知粒度,并关注性能和内存泄漏问题。通过明确角色、使用抽象和管理观察者注册,可最大化其效果。
|
3天前
|
设计模式 算法 Java
[设计模式Java实现附plantuml源码~行为型]定义算法的框架——模板方法模式
[设计模式Java实现附plantuml源码~行为型]定义算法的框架——模板方法模式
|
3天前
|
设计模式 JavaScript Java
[设计模式Java实现附plantuml源码~行为型] 对象状态及其转换——状态模式
[设计模式Java实现附plantuml源码~行为型] 对象状态及其转换——状态模式
|
3天前
|
设计模式 存储 JavaScript
[设计模式Java实现附plantuml源码~创建型] 多态工厂的实现——工厂方法模式
[设计模式Java实现附plantuml源码~创建型] 多态工厂的实现——工厂方法模式
|
3天前
|
设计模式 Java Go
[设计模式Java实现附plantuml源码~创建型] 集中式工厂的实现~简单工厂模式
[设计模式Java实现附plantuml源码~创建型] 集中式工厂的实现~简单工厂模式
|
3天前
|
设计模式 存储 前端开发
Java从入门到精通:2.2.1学习Java Web开发,了解Servlet和JSP技术,掌握MVC设计模式
Java从入门到精通:2.2.1学习Java Web开发,了解Servlet和JSP技术,掌握MVC设计模式