Observer Pattern Beginning
http://www.levilee.cn/Jeans/304/Play_9426_1/
1. 使用观察者(Observer)实现对象监听
观察者(Observer)是一种模式,也是Java中的一个API,它让一个值对象(Value Object)具备自省的功能,当他发现自己的状态改变了,就向相关的对象发送消息,这样的监听方式当然比轮询好。我感冒了自己会去医院,用不着医生每个月来问一次。
Java的Observer API是对观察者模式的一个实现。假设我们有一个对象容器,其中存放用户消息,我希望这个容器自省,当有新的消息进来就自动触发观察者作出响应。首先定义消息对象,是个很简单的值对象:
package com.gwnet.smsMessenger.mm.bromon;
public class Message
{
private int id;
private String sender;
private String receiver;
private String content;
private String time;
//请自己实现set/get方法,比如:
public int getId()
{
return id;
}
public void setId(int id)
{
this.id=id;
}
}
然后写一个存放Message的容器,容器使用ArrayList来存放对象是个很好的选择,也很简单:
package com.gwnet.smsMessenger.mm.bromon;
import java.util.*;
public class MessageList extends Observable
{
private List m=new ArrayList();
private static MessageList ml=null;
public MessageList()
{
}
public static MessageList getInstance()
{
if(ml==null)
{
ml=new MessageList();
}
return ml;
}
public void add(Message msg)
{
m.add(msg);
super.setChanged();
super.notifyObservers(m);
}
public void del (Message msg)
{
m.remove(msg);
}
}
这个类继承了Observable类,并且对其中的add方法做了手脚,很明显,add方法的作用是向ArrayList容器中放入一个对象,这正是我们想监听的操作,所以有了:
uper.setChanged();
super.notifyObservers(m);
这意思是一旦调用add方法,这个类自己就会向所有注册过的观察者发送消息,消息内容是什么呢?内容就是m,是存放消息的容器,观察者可以收到这个改变了状态的容器,然后对它进行操作,从而实现了对容器的监听,当然,我们只实现了对add方法的监听,你也可以试试其他的。
需要特别注意的是这是一个不完整的单例类,写成单例是为了要保证整个jvm中只有这一个存放消息的容器,而不写成完整的单例,原因是将来可能要提供另外的实例化方法。所以理解起来可能稍微难一点,大家可以参考一下设计模式中的单例模式。
下面就是编写观察者并且注册它:
package com.gwnet.smsMessenger.bromon;
import java.util.*;
public class MessageObserver implements Observer
{
public void update(Observable arg0, Object arg1)
{
List l=(List)arg1;
Message m=(Message)l.get(l.size()-1);
String receiver=m.getReceiver();
System.out.println("给"+m.getReceiver()+”的新消息:”+m.getContent());
}
}
这个类继承Oberver接口,update(Observable,Object)是必须提供的方法,在这个方法中我们接收被观察类传过来的数据(含有消息的容器),然后取出其中最后一个,读取它的内容。
Java里的观察者使用起来是非常简单的。我们的例子好处是所有的操作都在内存中进行,而且不需要轮询,效率非常高,缺点是一旦当机内存中的数据就丢失了,所以如果有一套比较完善的对象缓冲机制,就可以应付复杂的应用,写出高效简洁的多线程服务器。
http://www.cnblogs.com/goodcandle/archive/2006/04/05/observerext.html
2. Observer模式为何要区分推拉模式
先来比较两张UML图:
推模式
拉模式
两者的区别我再罗嗦一下,推模式是当通知消息来之时,把所有相关信息都通过参数的形式“推给”观察者。而拉模式是当通知消息来之时,通知的函数不带任何相关的信息,而是要观察者主动去“拉”信息。
推模式的优点:
是当消息来临时,观察者很直接地都到信息,然后进行相关地处理,与被观察者没有一点联系,两者几乎没有耦合。
推模式的缺点:
是当消息来临时,所有的信息都强迫观察者,不管有用与否。还有一个致命的缺点是,如果想在通知消息中添加一个参数,那么所有的观察者都需要修改了,这一点往往被忽视。
看来事物都有其两面性一点都不假,信息太全也不是一件好事。
“存在即有理由”,为了弥补推模式的不足,拉模式就诞生了。
就接着上面的例子,如果CPerson2想要都到秒的信息,按推模式来说,CPerson1也就需要修改了,然而用拉模式,各个观测者之间就没有什么联系了,因为具体的信息还要观测者主动去“拉”,而一旦有了主动权,各个观察者想拉什么信息就取决于具体的观察者了,这样CPerson1就无需修改了,只要在CNotifyBase中再添加一个接口函数就行了(GetSecond)。
Q:那CClockDevice不是还要修改吗?
A:修改是难免的,使用设计模式的目的不是不允许修改,而是让软件更易扩展,更易扩展体现在哪里呢?那就是让修改处尽可能的减少。看到UML图中那1和*了吗?你现在应该明白了吧?被观察者只要一个,而且不太会更改,而观察者确有很多。让你选择,你会选择修改什么呢?
当然拉模式的缺点也是存在的,那就是:
和被观察者有一定的耦合,但我们可以通过接口,把耦合降到最低。
下面是观察者模式其它一些优缺点:
1 ) 目标和观察者间的抽象耦合一个目标所知道的仅仅是它有一系列观察者, 每个都符合抽象的O b s e r v e r类的简单接口。目标不知道任何一个观察者属于哪一个具体的类。这样目标和观察者之间的耦合是抽象的和最小的。
因为目标和观察者不是紧密耦合的, 它们可以属于一个系统中的不同抽象层次。一个处于较低层次的目标对象可与一个处于较高层次的观察者通信并通知它, 这样就保持了系统层次的完整。如果目标和观察者混在一块, 那么得到的对象要么横贯两个层次(违反了层次性), 要么必须放在这两层的某一层中(这可能会损害层次抽象)。
2) 支持广播通信不像通常的请求, 目标发送的通知不需指定它的接收者。通知被自动广播给所有已向该目标对象登记的有关对象。目标对象并不关心到底有多少对象对自己感兴趣;它唯一的责任就是通知它的各观察者。这给了你在任何时刻增加和删除观察者的自由。处理还是忽略一个通知取决于观察者。
3) 意外的更新因为一个观察者并不知道其它观察者的存在, 它可能对改变目标的最终代价一无所知。在目标上一个看似无害的操作可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新。此外, 如果依赖准则的定义或维护不当,常常会引起错误的更新, 这种错误通常很难捕捉。
简单的更新协议不提供具体细节说明目标中什么被改变了, 这就使得上述问题更加严重。
如果没有其他协议帮助观察者发现什么发生了改变,它们可能会被迫尽力减少改变。
这一节讨论一些与实现依赖机制相关的问题。
1) 创建目标到其观察者之间的映射一个目标对象跟踪它应通知的观察者的最简单的方法是显式地在目标中保存对它们的引用。然而, 当目标很多而观察者较少时, 这样存储可能代价太高。一个解决办法是用时间换空间, 用一个关联查找机制(例如一个h a s h表)来维护目标到观察者的映射。这样一个没有观察者的目标就不产生存储开销。但另一方面, 这一方法增加了访问观察者的开销。
2) 观察多个目标在某些情况下, 一个观察者依赖于多个目标可能是有意义的。例如, 一个表格对象可能依赖于多个数据源。在这种情况下, 必须扩展U p d a t e接口以使观察者知道是哪一个目标送来的通知。目标对象可以简单地将自己作为U p d a t e操作的一个参数, 让观察者知道应去检查哪一个目标。
3) 谁触发更新目标和它的观察者依赖于通知机制来保持一致。但到底哪一个对象调用N o t i f y来触发更新? 此时有两个选择:
a) 由目标对象的状态设定操作在改变目标对象的状态后自动调用N o t i f y。这种方法的优点是客户不需要记住要在目标对象上调用N o t i f y,缺点是多个连续的操作会产生多次连续的更新, 可能效率较低。
b) 让客户负责在适当的时候调用N o t i f y。这样做的优点是客户可以在一系列的状态改变完成后再一次性地触发更新,避免了不必要的中间更新。缺点是给客户增加了触发更新的责任。由于客户可能会忘记调用N o t i f y,这种方式较易出错。
4) 对已删除目标的悬挂引用删除一个目标时应注意不要在其观察者中遗留对该目标的悬挂引用。一种避免悬挂引用的方法是, 当一个目标被删除时,让它通知它的观察者将对该目标的引用复位。一般来说, 不能简单地删除观察者, 因为其他的对象可能会引用它们, 或者也可能它们还在观察其他的目标。
5) 在发出通知前确保目标的状态自身是一致的在发出通知前确保状态自身一致这一点很重要, 因为观察者在更新其状态的过程中需要查询目标的当前状态。当S u b j e c t的子类调用继承的该项操作时, 很容易无意中违反这条自身一致的准则。你可以用抽象的S u b j e c t类中的模板方法( Template Method(5.10))发送通知来避免这种错误。定义那些子类可以重定义的原语操作, 并将N o t i f y作为模板方法中的最后一个操作, 这样当子类重定义了S u b j e c t的操作时,还可以保证该对象的状态是自身一致的。
6) 避免特定于观察者的更新协议-推/拉模型观察者模式的实现经常需要让目标广播关于其改变的其他一些信息。目标将这些信息作为U p d a t e操作一个参数传递出去。这些信息的量可能很小,也可能很大。一个极端情况是,目标向观察者发送关于改变的详细信息, 而不管它们需要与否。我们称之为推模型(push model)。另一个极端是拉模型(pull model); 目标除最小通知外什么也不送出,而在此之后由观察者显式地向目标询问细节。
拉模型强调的是目标不知道它的观察者, 而推模型假定目标知道一些观察者的需要的信息。
推模型可能使得观察者相对难以复用,因为目标对观察者的假定可能并不总是正确的。
另一方面。拉模型可能效率较差, 因为观察者对象需在没有目标对象帮助的情况下确定什么改变了。
7) 显式地指定感兴趣的改变你可以扩展目标的注册接口,让各观察者注册为仅对特定事件感兴趣,以提高更新的效率。当一个事件发生时, 目标仅通知那些已注册为对该事件感兴趣的观察者。支持这种做法一种途径是,对使用目标对象的方面(a s p e c t s)的概念。可用如下代码将观察者对象注册为对目标对象的某特定事件感兴趣:
void Subject::Attach(Observer*, Aspect& interest);
此处i n t e r e s t指定感兴趣的事件。在通知的时刻, 目标将这方面的改变作为U p d a t e操作的一个参数提供给它的观察者,例如:
void Observer::Update(Subject*, Aspect& interest);
8) 封装复杂的更新语义当目标和观察者间的依赖关系特别复杂时, 可能需要一个维护这些关系的对象。我们称这样的对象为更改管理器(C h a n g e M a n a g e r)。它的目的是尽量减少观察者反映其目标的状态变化所需的工作量。例如, 如果一个操作涉及到对几个相互依赖的目标进行改动, 就必须保证仅在所有的目标都已更改完毕后,才一次性地通知它们的观察者,而不是每个目标都通知观察者。
C h a n g e M a n a g e r有三个责任:
a) 它将一个目标映射到它的观察者并提供一个接口来维护这个映射。这就不需要由目标来维护对其观察者的引用, 反之亦然。
b) 它定义一个特定的更新策略。
c) 根据一个目标的请求, 它更新所有依赖于这个目标的观察者。
有两种特殊的C h a n g e M a n a g e r。S i m p l e C h a n g e M a n a g e r总是更新每一个目标的所有观察者, 比较简单。相反,D A G C h a n g e M a n a g e r处理目标及其观察者之间依赖关系构成的无环有向图。当一个观察者观察多个目标时, DAGChangeManager要比S i m p l e C h a n g e M a n a g e r更好一些。在这种情况下, 两个或更多个目标中产生的改变可能会产生冗余的更新。D A G C h a n g e M a n a g e r保证观察者仅接收一个更新。当然,当不存在多重更新的问题时, SimpleChangeManager更好一些。
C h a n g e M a n a g e r是一个M e d i a t o r ( 5 . 5 )模式的实例。通常只有一个C h a n g e M a n a g e r, 并且它是全局可见的。这里S i n g l e t o n ( 3 . 5 )模式可能有用。
9) 结合目标类和观察者类用不支持多重继承的语言(如S m a l l t a l k )书写的类库通常不单独定义S u b j e c t和O b s e r v e r类, 而是将它们的接口结合到一个类中。这就允许你定义一个既是一个目标又是一个观察者的对象,而不需要多重继承。例如在S m a l l t a l k中, Subject和O b s e r v e r接口