本文分为三个部分:
- Observer(观察者)
- Guava EventBus详解
- Guava EventBus使用示例
1. Observer(观察者)
1.1 背景
我们设计系统时,常常会将系统分割为一系列相互协作的类,使得这些类之间可以各自独立地复用,系统整体结构也会比较清晰。这是一种最基本的面向对象的设计方式,大多数情况下也非常有效。但是,如果这些相互协作的类之间的“协作”关系比较复杂,那么就会有副作用:需要维护这些类对象间的状态一致性。
我们以一个数据可视化系统为例来说明:
(1)数据可视化系统可以分割为两个主要的类:定义应用数据的类和负责界面表示的类;换言之,数据可视化系统中存在两类对象:应用数据对象(用于封装底层数据源中的数据)和界面对象(如:表格对象、柱形图对象、饼图对象等等);
(2)数据可视化系统中类(对象)之前的协作关系:如果应用数据对象发生变化(表示底层数据源中的数据发生变化),则表格对象、柱形图对象、饼图对象需要被立即更新;反过来也是如此,如果表格对象发生变化(假设表格对象的变化会导致底层数据源中的数据发生变化),则数据对象需要被立即更新,从而导致柱形图对象、饼图对象也需要被更新;
数据对象、表格对象、柱形图对象、饼图对象之间的协作关系如下:
表格对象、柱形图对象、饼图对象分别使用不同的表示形式描述同一个数据对象的信息,它们之间相互并不知道对方的存在,这样我们可以根据需要单独复用这些对象(类)。但在我们数据可视化系统的场景中,它们表现的“互相知道”(参见上述(2))。
这种“互相知道”的行为意味着表格对象、柱形图对象、饼图对象都需要依赖于数据对象,数据对象的任何状态变化都应立即通过它们,某一界面对象的变化实际也是通过数据对象反映给其它界面对象的。同时也没有理由将依赖于数据对象的界面对象数目限定为三个,对相同的数据可以有任意数目的不同用户界面。
综上所述,数据、表格、柱形图、饼图这几个互相协作的类的副作用表现为:需要维护数据对象、表格对象、柱形图对象(可能还有其它界面对象)之间状态的一致性,某一对象的状态发生变化,需要立即更新其它对象的状态。
1.2 定义
观察者模式用于定义对象间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都将得到通知并被“自动”更新。它有两个关键对象:目标(Subject)和观察者(Observer)。一个目标可以有任意数量的依赖它的观察者。一旦目标的状态发生改变,所有的观察者都得到通知。作为对这个通知的响应,每个观察者都将查询目标以使其状态与目标状态同步。
这种交互也称为“发布—订阅”(publish-subscribe)。目标是通知的发布者。它发出通知时并不需要知道谁是它的观察者。可以有任意数目的观察者订阅并接收通知。
注:参考1.1中的数据可视化系统示例,“目标”为数据对象,“观察者”为表格对象、柱形图对象、饼图对象。
1.3 结构与协作
Subject(目标)
—目标知道它的观察者,可以有任意多个观察者观察同一个目标。
—提供注册和删除观察者对象的接口(attach、detach)。
Observer(观察者)
—为那些在目标发生改变时需要获得通知的对象定义一个更新接口(update)。
ConcreteSubject(具体目标)
—将有关状态存入各个ConcreteSubject对象。
—当它的状态发生变化时,向它的各个观察者发出通知(notify)。
ConcreteObserver(具体观察者)
—维护一个指向ConcreteSubject对象的引用。
—存储有关状态,这些状态应与目标的状态保持一致。
—实现Observer的更新接口以使自身状态与目标的状态保持一致。
下面的交互图说明了一个目标和两个观察者之间的协作:
(1)当ConcreteSubject发生任何可能导致其观察者与其本身状态不一致的改变时(aConcreteSubject的改变请求由aConcreteObserver通过setState()发出),它将通知它的各个观察者(notify());
(2)ConcreteObserver对象在得到一个具体的改变通知后,可向目标对象查询信息(getState()),并使用这些信息使它的状态与目标对象的状态一致。
注意:
(1)发出改变请求的Observer对象并不立即更新,而是将其推迟到它从目标得到一个通知之后;
(2)notify()不总是由目标对象调用,它也可被一个观察者或其它对象调用;
1.4 更改管理器(ChangeManager)
当目标和观察者间的依赖关系特别复杂时,就需要一个维护这些关系的对象,我们称之为更改管理器(ChangeManager)。
ChangeManager有三个责任:
(1)它将一个目标映射到它的观察者并提供相应的接口(register、unregister)来维护这个映射,这就不需要由目标来维护对其观察者的引用,反之亦然;
(2)它定义一个特定的更新策略(这里的更新策略是更新所有依赖于这个目录的观察者);
(3)根据一个目标的请求(notify),它更新所有依赖于这个目标的观察者;
2. Guava EventBus详解
通常情况下,一个系统(这里特指进程)内部各个组件之间需要完成事件分发或消息通信,往往需要这些组件之间显式地相互引用。如果这些组件数目较多,且相互引用关系复杂就会出现副作用:需要维护这些相互引用的组件之间的状态一致性。
观察者模式(Observer)用于解决上述问题,EventBus就是该模式的一个实现,它是Google Guava提供的一个组件。
EventBus使用“发布—订阅”的通信模式,使得系统(进程)内各个组件之间无需相互显式引用即可完成事件分发或消息通信,如下图所求:
它的设计结构非常符合观察者模式,
目标:事件(Event);
观察者:事件监听器(EventListener)(EventHandler是EventListener的封闭);
每一次事件的发生或变化,EventBus负责将其派发(post)至相应的事件监听器,同一事件的事件监听器可以有多个。
2.1 EventBus register
2.1.1 作用
维护事件与事件监听器之间的对应关系,如果某一事件发生,可以从对应关系中查找出应该将该事件派发至哪些事件监听器。
2.1.2 事件(Event)与事件监听器(EventListener)
EventBus并不强制要求事件(Event)与事件监听器(EventListener)必须继承特定的类或实现特定的接口,普通的Java类即可。这是因为事件(Event)就是一个对象,它保存着特定时间点的特定状态,而事件监控器(EventListener)实质就是一个方法(Method),即发生特定事件就执行该方法,所以理论上这两者可以是任意的普通类。那么EventBus使用什么策略从一个普通的Java类中识别出事件(Event)与事件监听器(EventListener),从而维护它们之间的对应关系?既然是一个普通的Java类,那么策略应该是多种多样的,EventBus为此设计了一个策略接口:HandlerFindingStrategy,如下图所示:
这里首先说明一下EventHandler(事件处理器)。
如上所述,事件监控器(EventListener)实质就是一个方法(Method),而方法的调用需要目标对象target、目标方法method的共同参与,EventHandler对这两者信息进行了封装,后续讨论皆以事件处理器(EventHandler)表示事件监听器(EventListener)。
HandlerFindingStrategy仅仅有一个方法:Multimap<Class<?>, EventHandler> findAllHandlers(Object source),它代表着策略的抽象过程:从传入的类实例对象source中寻找出所有的事件(Event)与事件处理器(EventHandler)的对应关系。注意,该方法的返回值为Multimap,这是一种特殊的Map,一个键可以对应着多个值,它表示一个事件可以有多个事件处理器与之对应;其中,键为事件对象类实例,值为事件处理器实例。
目前,EventBus仅仅提供一种HandlerFindingStrategy的实现:AnnotatedHandlerFinder,它是一种基于注解(Annotation)的实现,
以类实例对象listener为例说明一下工作过程:
(1)获取实例对象listener的类实例clazz;
(2)获取类实例clazz的所有方法,并依次迭代处理,假设其中的一个方法为method:
a. 如果method标记有注解“Subscribe”,且method只有一个参数,则表示method可以作为事件监听器,继续处理;否则继续处理下一个method;
b. method的这个参数类型即为事件类型eventType;
c. 通过makeHandler()将实例对象listener、方法method封装为handler(事件处理器);
d. 维护eventType、handler之间的对应关系,将其保存至methodsInListener;
(3)获取类实例clazz的父类实例,将其保存至clazz,如果clazz不为null,则继续(2);否则结束;
makeHandler()工作过程实际就是构建EventHandler对象,如下所示:
如果方法method标记有注解AllowConcurrentEvents,则表示该方法可以被事件处理器在多线程环境下线程安全的访问,直接使用EventHandler封装即可;如果方法method没有标记有注解AllowConcurrentEvents,则表示该方法无法被事件处理器在多线程环境下线程安全的访问,需要使用SynchronizedEventHandler封装。SynchronizedEventHandler继承自EventHandler,仅有一处不同:
即使用关键字synchronized修饰方法handleEvent,使其可以在多线程环境下被安全地访问。
有几点需要注意:
(1)事件与事件处理器之间的对应关系是依靠事件类型(eventType)连接起来的,而事件类型(eventType)就是事件监听器方法的参数类型;
(2)类实例对象listener的任何一个方法,只要它含有注解Subscribe且只有一个参数,就可以作为事件监听器或事件处理器;
(3)类实例对象的所有父类都会参与上述工作过程;
2.1.3 register
handlersByType:SetMultimap实例,用于维护EventBus内部所有的事件与事件处理器的对应关系(SetMultimap、Multimap的使用方法可以参考Google Guava的相关文档);
finder:AnnotatedHandlerFinder实例;
object:类实例对象,用于从中寻找出事件与事件处理器的对应关系;handlersByType某一事件对应的事件处理器可能来自于不同的类实例对象object;
2.2 EventBus unregister
EventBus unregister就是从handlersByType中移除类实例对象object中包含的所有事件与事件处理器的对应关系,工作过程比较简单,不再赘述。
2.3 EventBus post
EventBus post大致可以分为以下三个过程:
2.3.1 flattenHierarchy
EventBus post event时,event整个继承关系树中所有类和接口对应的事件处理器都会参考到事件派发的过程中来,flattenHierarchy就是用于获取event整个继承关系树中所有类和接口的类实例的,每一个类实例(Class)表示一个事件类型:
因为这个继承关系树在系统(进程)的运行过程中不会发生变化(不考虑热加载的情况),这里使用了缓存技术,用于缓存某个对象的继承关系树,使用Google Guava LoadingCache构建,我们不对此详细展开讨论,仅仅阐述缓存没有命中时的处理情况:
可以看出,整个继承关系树中的类和接口都被获取。
2.3.2 enqueueEvent
依次为每个事件类型对应的事件处理器派发事件,此时事件处理器并没有被实际执行,而是以EventWithHandler对象的形式被存入一个队列。
getHandlersForEventType:用于获取事件类型对应的所有事件处理器;
enqueueEvent:用于将事件(Event)和事件处理器(EventHandler)封装为EventWithHandler放入队列;
EventWithHandler如下:
enqueueEvent如下:
eventsToDispatch是一个ThreadLocal<ConcurrentLinkedQueue<EventWithHandler>>变量,也就是每一个post线程内部都有一个队列,用于存放EventWithHandler对象。
疑问:是否需要使用ConcurrentLinkedQueue?
2.3.3 dispatchQueuedEvents
dispatchQueuedEvents的过程其实就是执行队列中的事件处理器,过程如下:
可以看出,队列中的事件处理器是依次被执行的。
疑问:是否需要使用isDispatching?
EventBus是一种同步实现(即事件处理器是被依次触发的),另外有一种异步实现AsyncEventBus,核心原理相同,有兴趣的读者可自行研究。
3. Guava EventBus使用示例
输出结果:
EventHandler handle Event
EventHandler2 handle Event2
分类: Design Patterns, Java