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

工具与资源中心

帮助开发者更加高效的工作,提供围绕开发者全生命周期的工具与资源https://developer.aliyun.com/tool?spm=a1z389.11499242.0.0.65452413shiknb&utm_content=g_1000284493

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

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

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

二、基本概念

1. 定义

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

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

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

  • 目标对象:即被依赖的对象或被观察的对象,当状态发生变更时会通知所有的观察者对象。在上面的例子中,交通灯就是被观察的对象;

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

  其结构图如下:

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

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

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

  • 事件源:即最初发生事件的对象,也对应者观察者模式中被观察的目标对象;

  • 事件对象:即被触发的事件,事件对象需要有能够执行该事件的主体,即事件源;

  • 事件监听者:即监听发生事件的对象,当监听的对应对象发生某个事件之后,事件监听者会根据发生的事件做出预先设定好的相应举措;

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

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

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

三、源码探究

1. JDK中的观察者模式

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

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

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

public EventObject(Object source) {
if (source == null)
throw new IllegalArgumentException("null source");
this.source = source;
}

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

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

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

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

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

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

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

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

@Override
public void publishEvent(ApplicationEvent event) {
publishEvent(event, null);
}

protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
Assert.notNull(event, "Event must not be null");
// 将事件包装成ApplicationEvent
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent) {
applicationEvent = (ApplicationEvent) event;
} else {
applicationEvent = new PayloadApplicationEvent<>(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.parent instanceof AbstractApplicationContext) {
((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
}
else {
this.parent.publishEvent(event);
 }
 }
 }
 /**
 * 将事件广播给对应的监听者
 */
 public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
 ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
 Executor executor = getTaskExecutor();
 for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
 if (executor != null) {
 executor.execute(() -> invokeListener(listener, event));
 }
 else {
 invokeListener(listener, event);
 }
 }
 }

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

四、总结

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

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

(0)

相关推荐

  • 无废话设计模式(15)行为型模式--观察者模式

    0-前言 观察者模式定义:定义了一种一对多的依赖关系让多个观察者对象同事监听某一主题对象. 这个主题对象在状态发生改变时,会通知所有观察者对象,使他们能够自动更新自己: 1-实现 1-1.简单UML图 ...

  • 观察者模式最佳实践,构建自己的一套事件分发系统

    前言 试想这样一个问题,当某个事件发生时,比如在游戏中A模块修改了用户的金币数,而B模块和C模块提供的功能都依赖于用户的金币数,那么,A模块在修改金币数的同时,就需要通知B模块和C模块.常规的方法就是 ...

  • 谈谈Java常用类库中的设计模式 - Part Ⅲ

    概述 本系列上一篇:适配器.模版方法.装饰器 本文介绍的设计模式: 策略 观察者 代理 相关缩写:EJ - Effective Java Here We Go 策略 (Stragety) 定义:定义算 ...

  • 谈谈Java常用类库中的设计模式 - Part Ⅱ

    概述 本系列上一篇:建造者.工厂方法.享元.桥接 本文介绍的设计模式(建议按顺序阅读): 适配器 模板方法 装饰器 相关缩写:EJ - Effective Java Here We Go 适配器 (A ...

  • 谈谈Java常用类库中的设计模式 - Part Ⅰ

    背景 最近一口气看完了Joshua Bloch大神的Effective Java(下文简称EJ).书中以tips的形式罗列了Java开发中的最佳实践,每个tip都将其意图和要点压缩在了标题里,这种做法 ...

  • 图解Java设计模式之观察者模式

    天气预报项目需求 1)气象站可以将每天测量到的湿度.温度.气压等等以公告的形式发布出去(比如发布到自己的网站或第三方). 2)需要设计开放型API,便于其他第三方也能接入气象站获取数据. 3)提供温度 ...

  • JAVA设计模式之观察者模式

    JAVA设计模式之观察者模式

  • JAVA设计模式之观察者模式 - Observer

    有趣的事情发生时,可千万别错过了!有一个模式可以帮你的对象知悉现况,不会错过该对象感兴趣的事.对象甚至在运行时可决定是否要继续被通知.有了观察者,你将会消息灵通. 介绍 观察者模式的定义: 在对象之间 ...

  • 设计模式 - Java中单例模式的6种写法及优缺点对比

    目录 1 为什么要用单例模式 1.1 什么是单例模式 1.2 单例模式的思路和优势 2 写法① - 饥饿模式 2.1 代码示例 2.2 优缺点比较 3 写法② - 懒惰模式 3.1 代码示例 3.2 ...

  • Java 中的关键字

    Java 中有多少个关键字,有大小写之分吗? Java 中有 48 个关键字在使用 + 两个保留关键字未使用,共 50 个关键字. Java 关键字全部都由是小写组成. Java 中保留关键字分别是哪 ...

  • Java中的匿名内部类

    一.匿名内部类 之前的所有类都有自己的名字,但是有时候如果某个接口的实现类(或者某个父类的子类)只需要使用一次,此时这样的类如果我们单独定义出来则需要单独为其创建一个"*.java" ...