OBSERVER(观察者) ———— 对象行为型模式
意图
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
功能
Observer模式中的关键对象是目标(subject)和观察者(observer)。一个目标可以有任意数目的依赖它的观察者。一旦目标的状态发生改变,所有的观察者都得到通知。作为对这个通知的响应,每个观察者都将查询目标以使其状态与目标的状态同步。
这种交互也称为发布-订阅(publish-subscribe)。目标是通知的发布者。它发出通知时并不需要知道谁是它的观察者。可以有任意数目的观察者订阅并接受通知。
适用性
在以下任一情况下可以使用观察者模式
① 当一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
② 当对一个对象的改变需要同时改变其它对象,而不知道具体有多少对象有待改变。
③ 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之,你不希望这些对象是紧密耦合的。
结构
-
Subject(目标)
目标知道它的观察者。可以有任意多个观察者观察同一个目标。
提供注册和删除观察者对象的接口。 -
Observer(观察者)
为那些在目标发生改变时需要获得通知的对象定义一个更新接口。 -
ConcreteSubject(具体目标)
将有关状态存入各ConcreteObserver对象。
当它的状态发生改变时,向它的各个观察者发出通知。 -
ConcreteObserver(具体观察者)
维护一个指向ConcreteSubject对象的引用。
存储有关状态,这些状态应与目标的状态保持一致。
实现Observer的更新接口以使自身状态与目标的状态保持一致。
优缺点
优点
① 目标和观察者的抽象耦合:一个目标所知道的仅仅是它又一系列观察者,每个都符合抽象的Observer类的简单接口。目标不知道任何一个观察者属于哪一个具体的类。这样目标和观察者之间的耦合是抽象的和最小的。
② 支持广播通信:不像通常的请求,目标发送的通知不需指定它的接收者。通知被自动广播给所有已向该目标对象登记的有关对象。目标对象并不关心到底有多少对象对自己感兴趣;它唯一的责任就是通知它的各观察者。这给了你在任何时刻增加和删除观察者的自由。处理还是忽略一个通知取决于观察者。
缺点
① 意外的更新:因为一个观察者并不知道其他观察者的存在,它可能对改变目标的最终代价一无所知。在目标上一个看似无害的操作可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新。此外,如果依赖准则的定义或维护不当,常常会引起错误的更新,这种错误通常很难捕捉。
对观察者模式实现的深入探讨
- 目标与观察者之间的映射:通常会在目标对象中采用一个集合来保存观察者的注册信息。
- 观察多个目标:在某些情况下,一个观察者依赖于多个目标可能是有意义的。
- 谁触发更新:目标和它的观察者依赖于通知机制来保持一致。但到底哪一个对象调用Notify来触发更新?此时有两个选择:
① 由目标对象的状态设定操作在改变目标对象的状态后自动调用Notify。这种方法的优点是客户不需要记住要在目标对象上调用Notify,缺点是多个连续的操作会产生多次连续的更新,可能效率较低。
② 让客户负责在适当的时候调用Notify。这样做的优点是客户可以在一系列的状态改变完成后再一次性地触发更新,避免了不必要的中间更新。缺点是给客户增加了触发更新的责任。由于客户可能忘记调用Notify,这种方式较易出错。 - 对已删除目标的悬挂引用:删除一个目标时应注意不要在其观察者中遗留对该目标的悬挂引用。这种避免悬挂引用的方法是,当一个目标被删除时,让它通知它的观察者将对该目标的引用复位。一般来说,不能简单地删除观察者,因为其他的对象可能会引用它们,或者也可能它们还在观察其他的目标。
- 在发出通知前确保目标的状态自身是一致的:在实现观察者模式的时候,一定要注意触发通知的时机,一般情况下,是在完成了状态维护后触发,因为通知会传递数据,不能够先通知后改数据,这很容易出问题,会导致观察者和目标对象的状态不一致。比如:目标一发出通知,就有观察者来取值,结果目标还没有更新数据,这就明显造成了错误。
- 显示地定义感兴趣的改变:你可以扩展目标的注册接口,让各观察者注册为仅对特定事件感兴趣,以提高更新的效率。当一个事件发生时,目标仅通知那些已注册为对该事件感兴趣的观察者。支持这种做法一种途径是,使用目标对象的方面(aspects)的概念。
-
推模型和拉模型
① 推模型:
目标对象主动向观察者推送目标的详细信息,不管观察者是否需要,推送的信息通常是目标对象的全部或部分数据。
② 拉模型:
目标对象在通知观察者的时候,只传递少量信息,如果观察者需要更具体的信息,由观察者主动到目标对象中获取,相当于是观察者从目标对象中拉数据。
一般这种模型的实现中,会把目标对象自身通过update方法传递给观察者,这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。
推模型 VS 拉模型
a) 推模型是假定目标对象知道观察者需要的数据;而拉模型是目标对象不知道观察者具体需要什么数据,没有办法的情况下,干脆把自身传给观察者,让观察者自己去按需取值。
b) 推模型可能会使得观察者对象难以复用,因为观察者定义的update方法是按需而定义的,可能无法兼顾没有考虑到的使用情况。这就意味着出现新情况的时候,就可能需要提供新的update方法,或者是干脆重新实现观察者。
c) 而拉模型就不会造成这样的情况,因为拉模型下,update方法的参数是目标对象本身,这基本上是目标对象能传递的最大数据集合了,基本上可以适应各种情况的需要。
Java中的观察者模式
Java API 有内置的观察者模式。java.util包(package)内包含最基本的Observer接口与Observable类,这和我们的Subject接口与Observer接口很相似。Observer接口与Observable类使用上更方便,因为许多功能都已经事先准备好了。你甚至可以使用推或拉的方式传送数据。
我们可以通过继承Observable类和实现Observer接口来分别实现主题和观察者。然后通过调用Observable对象的addObserver()方法来添加观察者,调用deleteObserver()来移除一个观察者。
Q:Observable要如何送出通知?
A: 需要两个步骤:
① 先调用setChanged()方法,标记状态已经改变的事实。
② 然后调用两种notifyObservers方法中的一个:
a) notifyObsrvers() ———— 用于拉模型;
b) notifyObserves(Object arg) ———— 用于推模型;
Q:Observer如何接受通知?
A:同以前一样,观察者实现了更新的方法,但是方法的签名不太一样:
void update(Observable o, Object arg);
参数 Observable o :主题本身当作第一个变量,好让观察者知道是哪个主题通知它的。若是拉模式,则需要通过该参数来获取所需的数据。
参数 Object arg :若Observable通过notifyObsrvers()方式发送通知,则该参数为null;若Observable通过notifyObserves(Object arg)方式发送通知,则该参数为推送过来的数据对象。
Q:为什么在Observable发送notifyObservers前一定要调动setChanged()方法来标记状态已经改变的事实了?
A:从notify的源码能看出,若状态标志changed未被设置为true,则不会进行对Observer的notify操作了。而setChanged()方法就是来完成对状态标志changed的设置的。
而这么做的好处在于,让你在更新观察者时有更多的弹性,你可以更适当地通知观察者。
参考
《Head First 设计模式》
《设计模式:可复用面向对象软件的基础》
《研磨设计模式》