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
三个基础类来支持发布/订阅模式。其中 ApplicationEvent
和 ApplicationListener
分别继承了 EventObject
和 EventListener
,其作用也和这两个类相同,就不再过多赘述。这里具体关注一下 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中的事件处理如果存在线程池的话,还使用了线程池就行异步处理对应的事件,进一步将发布者和监听者做了解耦。
四、总结
观察者模式最大的特定是建立了一个一对多且松散的耦合关系,观察目标只需要维持一个抽象观察者集合,无须感知具体的观察者有哪些。这样一个松散的耦合关系有利于观察目标和观察者各自进行对应的抽象处理,很好的体现了开闭原则。
当然,观察者模式也有其弊端,比如只定义了一对多的关系,无法处理多对多的场景;又比如只能感知观察目标发生了变化,但是具体如何变化却无法了解到,等等。这些都是观察者模式无法处理的场景或存在的问题。