全面解析Java8观察者模式

这篇文章主要为大家全面解析Java8观察者模式,通过在 Java8 环境下实现观察者模式的实例,进一步介绍了什么是观察者模式、专业化及其命名规则,感兴趣的小伙伴们可以参考一下

观察者(Observer)模式又名发布-订阅(Publish/Subscribe)模式,是四人组(GoF,即 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides)在1994合著的《设计模式:可复用面向对象软件的基础》中提出的(详见书中293-313页)。尽管这种模式已经有相当长的历史,它仍然广泛适用于各种场景,甚至成为了标准Java库的一个组成部分。目前虽然已经有大量关于观察者模式的文章,但它们都专注于在 Java 中的实现,却忽视了开发者在Java中使用观察者模式时遇到的各种问题。

本文的写作初衷就是为了填补这一空白:本文主要介绍通过使用 Java8 架构实现观察者模式,并在此基础上进一步探讨关于经典模式的复杂问题,包括匿名内部类、lambda 表达式、线程安全以及非平凡耗时长的观察者实现。本文内容虽然并不全面,很多这种模式所涉及的复杂问题,远不是一篇文章就能说清的。但是读完本文,读者能了解什么是观察者模式,它在Java中的通用性以及如何处理在 Java 中实现观察者模式时的一些常见问题。

观察者模式
根据 GoF 提出的经典定义,观察者模式的主旨是:

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

什么意思呢?很多软件应用中,对象之间的状态都是互相依赖的。例如,如果一个应用专注于数值数据加工,这个数据也许会通过图形用户界面(GUI)的表格或图表来展现或者两者同时使用,也就是说,当底层数据更新时,相应的 GUI 组件也要更新。问题的关键在于如何做到底层数据更新时 GUI 组件也随之更新,同时尽量减小 GUI 组件和底层数据的耦合度。

一种简单且不可扩展的解决方案是给管理这些底层数据的对象该表格和图像 GUI 组件的引用,使得对象可以在底层数据变化时能够通知 GUI 组件。显然,对于处理有更多 GUI 组件的复杂应用,这个简单的解决方案很快显示出其不足。例如,有20个 GUI 组件都依赖于底层数据,那么管理底层数据的对象就需要维护指向这20个组件的引用。随着依赖于相关数据的对象数量的增加,数据管理和对象之间的耦合度也变得难以控制。

另一个更好的解决方案是允许对象注册获取感兴趣数据更新的权限,当数据变化时,数据管理器就会通知这些对象。通俗地说就是,让感兴趣的数据对象告诉管理器:“当数据变化时请通知我”。此外,这些对象不仅可以注册获取更新通知,也可以取消注册,保证数据管理器在数据变化时不再通知该对象。在 GoF 的原始定义中,注册获取更新的对象叫作“观察者”(observer),对应的数据管理器叫作“目标”(Subject),观察者感兴趣的数据叫作“目标状态”,注册过程叫“添加”(attach),撤销观察的过程叫“移除”(detach)。前文已经提到观察者模式又叫发布-订阅模式,可以理解为客户订阅关于目标的观察者,当目标状态更新时,目标把这些更新发布给订阅者(这种设计模式扩展为通用架构,称为发布――订阅架构)。这些概念可以用下面的类图表示:

具体观察者(ConcereteObserver)用来接收更新的状态变化,同时将指向具体主题(ConcereteSubject)的引用传递给它的构造函数。这为具体观察者提供了指向具体主题的引用,在状态变化时可由此获得更新。简单来说,具体观察者会被告知主题更新,同时用其构造函数中的引用来获取具体主题的状态,最后将这些检索状态对象存储在具体观察者的观察状态(observerState)属性下。这一过程如下面的序列图所示:

经典模式的专业化
尽管观察者模式是通用的,但也有很多专业化的模式,最常见是以下两种:

1、为State对象提供一个参数,传给观察者调用的Update方法。在经典模式下,当观察者被通知Subject状态发生变化后,会直接从Subject获得其更新后状态。这要求观察者保存指向获取状态的对象引用。这样就形成了一个循环引用,ConcreteSubject的引用指向其观察者列表,ConcreteObserver的引用指向能获得主题状态的ConcreteSubject。除了获得更新的状态,观察者和其注册监听的Subject间并没有联系,观察者关心的是State对象,而非Subject本身。也就是说,很多情况下都将ConcreteObserver和ConcreteSubject强行联系一起,相反,当ConcreteSubject调用Update函数时,将State对象传递给ConcreteObserver,二者就无需关联。ConcreteObserver和State对象之间关联减小了观察者和State之间的依赖程度(关联和依赖的更多区别请参见Martin Fowler's的文章)。

2、将Subject抽象类和ConcreteSubject合并到一个 singleSubject类中。多数情况下,Subject使用抽象类并不会提升程序的灵活性和可扩展性,因此,将这一抽象类和具体类合并简化了设计。

这两个专业化的模式组合后,其简化类图如下:

在这些专业化的模式中,静态类结构大大简化,类之间的相互作用也得以简化。此时的序列图如下:

专业化模式另一特点是删除了 ConcreteObserver 的成员变量 observerState。有时候具体观察者并不需要保存Subject的最新状态,而只需要监测状态更新时 Subject 的状态。例如,如果观察者将成员变量的值更新到标准输出上,就可以删除 observerState,这样一来就删除了ConcreteObserver和State类之间的关联。

更常见的命名规则
经典模式甚至是前文提到的专业化模式都用的是attach,detach和observer等术语,而Java实现中很多都是用的不同的词典,包括register,unregister,listener等。值得一提的是State是listener需要监测变化的所有对象的统称,状态对象的具体名称需要看观察者模式用到的场景。例如,在listener监听事件发生场景下的观察者模式,已注册的listener将会在事件发生时收到通知,此时的状态对象就是event,也就是事件是否发生。

平时实际应用中目标的命名很少包含Subject。例如,创建一个关于动物园的应用,注册多个监听器用于观察Zoo类,并在新动物进入动物园时收到通知。该案例中的目标是Zoo类,为了和所给问题域保持术语一致,将不会用到Subject这样的词汇,也就是说Zoo类不会命名为ZooSubject。

监听器的命名一般都会跟着Listener后缀,例如前文提到的监测新动物加入的监听器会命名为AnimalAddedListener。类似的,register,、unregister和notify等函数命名常会以其对应的监听器名作后缀,例如AnimalAddedListener的register、unregister、notify函数会被命名为registerAnimalAddedListener、 unregisterAnimalAddedListener和notifyAnimalAddedListeners,需要注意的是notify函数名的s,因为notify函数处理的是多个而非单一监听器。

这种命名方式会显得冗长,而且通常一个subject会注册多个类型的监听器,如前面提到的动物园的例子,Zoo内除了注册监听动物新增的监听器,还需注册监听动物减少监听器,此时就会有两种register函数:(registerAnimalAddedListener和 registerAnimalRemovedListener,这种方式处理,监听器的类型作为一个限定符,表示其应观察者的类型。另一解决方案是创建一个registerListener函数然后重载,但是方案一能更方便的知道哪个监听器正在监听,重载是比较小众的做法。

另一惯用语法是用on前缀而不是update,例如update函数命名为onAnimalAdded而不是updateAnimalAdded。这种情况在监听器获得一个序列的通知时更常见,如向list中新增一个动物,但很少用于更新一个单独的数据,比如动物的名字。

接下来本文将使用Java的符号规则,虽然符号规则不会改变系统的真实设计和实现,但是使用其他开发者都熟悉的术语是很重要的开发准则,因此要熟悉上文描述的Java中的观察者模式符号规则。下文将在Java8环境下用一个简单例子来阐述上述概念。

一个简单的实例
还是前面提到的动物园的例子,使用Java8的API接口实现一个简单的系统,说明观察者模式的基本原理。问题描述为:

创建一个系统zoo,允许用户监听和撤销监听添加新对象animal的状态,另外再创建一个具体监听器,负责输出新增动物的name。

根据前面对观察者模式的学习知道实现这样的应用需要创建4个类,具体是:

  • Zoo类:即模式中的主题,负责存储动物园中的所有动物,并在新动物加入时通知所有已注册的监听器。
  • Animal类:代表动物对象。
  • AnimalAddedListener类:即观察者接口。
  • 赞(0) 打赏
未经允许不得转载:0133技术站首页 » Java