写在前面
Observer(发布订阅机制)是MVC架构的基石,是非常普遍的行为型设计模式之一。
本文简单讨论了Observer设计模式在不同层次上的具体实现。
Java基础库中有一个Observable抽象类和一个Observer接口。这是一套基本的Observer实现,可以满足很多应用。但是Observable毕竟是抽象类,Java不能多根继承,用组合代替继承也需要暴露接口,并且函数签名都已经固定,所以有时还需要自己实现Observer模式。
Observer模式在实现中更多的被叫作Event/Listener。
简单玩具
最简单的一种实现可以是这样的:
[ 实现一 ]:
public class Subject
{
protected List listeners = new ArrayList();
public void addListener(IListener listener) {
listeners.add(listener);
}
public void removeListener(IListener listener) {
listeners.remove(listener);
}
protected void fireChanged() {
for(Iterator i = listeners.iterator(); i.hasNext();)
((IListener)i.next()).onSubjectChanged(this);
}
somefunction() {
... sth changed
fireChanged();
}
}
public interface IListener {
public void onSubjectChanged(Subject subject);
}
这个基本的Observer可以有很多变形:
l 接口的函数个数:可以是多个,他们监听一类消息。
多函数的Listener接口有时候看上去很丑陋,因为某些时候有些订阅者只需要其中一部分消息,而另一部分消息是没有用的。
C#中有Delegate机制可以解决这个问题,而Java中Eclipse架构里利用Adapter设计模式,提出了ListenerAdapter。
ListenerAdapter是继承IListener接口的抽象类,对每一个发布的消息都有默认的响应,实际订阅者需要Override所需要订阅的消息即可。
l 订阅消息的参数:不一定非要是Subject,也可以是其他的特定消息。这一点在后面MVC模式中详述。
基本规范
[实现一]是有缺陷的:
1. ArrayList数据结构不是线程安全的。
2. 没有对已有的Listener做检查,可能会有重复的Listener。
public void addListener(IListener listener) {
listeners.add(listener);
}
3. 这两个函数会引起线程冲突。
public void removeListener(IListener listener) {
listeners.remove(listener);
}
protected void fireChanged() {
for(Iterator i = listeners.iterator(); i.hasNext();)
((IListener)i.next()).onSubjectChanged(this);
}
比如:
public static void main(String argv[]) {
Subject sub = new Subject();
sub.addListener(new IListener() {
public void onSubjectChanged(Subject subject) {
subject.remove(this); //冲突
}
}
);
}
这个冲突来源于迭代器设计模式本身的缺陷:在遍历一个聚合的同时更改这个聚合可能是危险的。一个健壮的迭代器(robust iterator)保证插入和删除操作不会干扰遍历, 且不需拷贝该聚合。[1]
Java中没有彻底解决这个缺陷,Collection迭代过程中,Collection对象的monitor被迭代器把持,不能使用Collection的其他方法修改Collection。
那么,把迭代过程改成这样呢?
protected void fireChanged() {
for(int i = 0; i < listeners.size(); i ++)
((IListener)listeners.get(i)).onSubjectChanged(this);
}
这样就失去了迭代器对Collection的保护,迭代过程中可以随意修改Collection,使得结果会更糟糕。
参考eclipse中的ListenerList结构和Rhino中的Tool提供的ToolKit可知。一个比较可行的方法是这样的,把监听列表复制一遍,迭代一份拷贝,另一份仍然可以支持外部修改:
protected void fireChanged() {
IListener[] ls = (IListener[])listeners.toArray(new
Listener[listeners.size()]);
for(int i = 0; i < ls.length; i ++)
ls[i].onSubjectChanged(this);
}
这样虽然浪费了一些时间和空间,但是换回了迭代过程的健壮性。
我们再把上面两个问题解决掉,仅使用Java基础库,可以做出如下实现:
[ 实现二 ] :
public class Subject {
protected List listeners = new
Collections.synchronizedList(new ArrayList());
public void addListener(IListener listener) {
if (!listeners.contains(listener))
listeners.add(listener);
}
public void removeListener(IListener listener) {
listeners.remove(listener);
}
protected void fireChanged() {
IListener[] ls = (IListener[]) listeners
.toArray(new IListener[listeners.size()]);
for (int i = 0; i < ls.length; i++)
ls[i].onSubjectChanged(this);
}
somefunction() {
... sth changed
fireChanged();
}
}
public interface IListener {
public void onSubjectChanged(Subject subject);
}
高级功能
以上实现可以满足一般的要求,下面考虑一些高级要求:
1. 健壮性
有时候某个Listener出了毛病,抛出异常之后,排在后面的Listener就不能正常收到通知了。eclipse中有个SafeRunnable模式可以解决这个问题。
这是一段核心代码:
private static ISafeRunnableRunner createDefaultRunner() {
return new ISafeRunnableRunner() {
public void run(ISafeRunnable code) {
try {
code.run();
} catch (Exception e) {
handleException(code, e);
} catch (LinkageError e) {
handleException(code, e);
}
}
private void handleException(ISafeRunnable code, Throwable e) {
if (!(e instanceof OperationCanceledException)) {
e.printStackTrace();
}
code.handleException(e);
}
};
}
这是一个使用SafeRunnable的一个例子:
protected void fireAttributeAdded(final nsIDOMMutationEvent event) {
Object[] listeners = domMutationListeners.getListeners();
for (int i = 0; i < listeners.length; ++i) {
final IDOMMutationListener l =
(IDOMMutationListener) listeners[i];
SafeRunnable.run(new SafeRunnable() {
public void run() {
l.attributeAdded((nsIDOMElement)
MozillaTools.SafeQuery(event.getTarget(),
nsIDOMElement.NS_IDOMELEMENT_IID),
event.getAttrName());
}
});
}
}
注意:SafeRunnable与Java基础类库中的Runnable不同.前者只是隔离异常,后者目的却是开一个新线程。
调用Listener的onSubjectChanged不需要开启新线程,因为任务不大的时候开了没有必要; 任务大的时候,写Listener的开发者完全可以自己会主动开线程。
需要的话,可以把发布函数写的安全性高一些。
protected void fireChanged() {
IListener[] ls = (IListener[])listeners.toArray(new
IListener[listeners.size()]);
for(int i = 0; i < ls.length; i ++) {
try {
ls[i].onSubjectChanged(this);
} catch(Throwable e) {
e.printStackTrace();
//Advanced Handler can be added here.
}
}
}
2. Listener死循环
Listener可能会在监听的时候改变Subject,然后触发再一次发布,然后再听到,再针对其改变,再发布,无限循环。
在我看来,这种情况应该尽量避免,如果不得已非得在监听的时候对Subject进行修改,那么就需要一些特殊手段。
这个问题MFC是通过传递参数来解决的,谁修改的,就不发给谁了。
不过,这种传递参数显得累赘,其实如果没有这个参数也可以具体问题具体分析,找到别的解决方案。
一种解决方案是:再次发布的时候不进行和上一次修改的最终状态一致就不进行修改,避免循环。
另一种可行的解决方案是:在修改之前把自己监听取消,修改之后再挂上。
new IListener() {
public void onSubjectChanged(Subject subject) {
subject.removeListener(this);
...modify subject
subject.addListener(this);
}
}
再一种可行的方案是增加一个间接层,可以让View控制Controller,再由Controller去修改Model,在中间虑掉这个View的Listener,这个方案和MFC的解决方案本质上相同。
3. MVC数据保护
在MVC模式中Model提供了读取和修改的方法。读取主要提供给View,修改主要提供给Controller。最简单的Model可以以JavaBean为例。
有些Listener(即View)不应该修改Model,我们需要控制View对Model的修改。
如果我们把Model传给listener,就没办法控制Listener是否修改。
Java没有const关键字,不能传递const型参数;如果只传递一个只提供getter方法的接口,Listener也有可能对它做强制转化,使用Model提供给Controller的方法。所以,需要另想办法来解决。
可以考虑类似Servlet中HttpRequest的数据保护方式,在Model外面再加一层外包。
整理一下:
[ 实现三 ] :
public interface IListener {
public void onModelChanged();
}
public interface IModel {
public String getSampleField();
}
public class Model {
protected String sampleField;
protected List listeners = new Collections.synchronizedList(
new ArrayList());
public void addListener(IListener listener) {
if(!listeners.contains(listener))
listeners.add(listener);
}
public void removeListener(IListener listener) {
listeners.remove(listener);
}
public String getSampleField() {
return sampleField;
}
public void setSampleField(String sampleField) {
this.sampleField = sampleField;
fireModelChanged();
}
protected fireModelChanged() {
IListener[] ls = (IListener[])listeners.toArray(new
IListener[listeners.size()]);
final ReadOnlyModelWrapper model = new
ReadOnlyModelWrapper(this);
for(int i = 0; i < ls.length; i ++) {
try {
ls[i].onModelChanged(model);
} catch(Throwable e) {
e.printStackTrace();
//Advanced Handler can be added here.
}
}
}
}
public class ReadOnlyModelWrapper {
private IModel model;
public ReadOnlyModelWrapper(IModel model) {
this.model = model;
}
public String getSampleField() {
return model.getSampleField();
}
}
其他 – UI线程中的Observer
在使用SWT等UI库时,需要注意UI线程和普通线程的不同。UI线程从本质上讲就是有消息循环的线程,普通线程就是没有消息循环的线程。UI线程发布事件的处理也是在UI线程内部,这时可以使用UI线程中的资源。但是如果使用其他线程就不可以使用UI线程中的资源。
Observer设计模式在设置消息发布的时机要设置的得当。举一个反例:Mozilla在XUL的Browser实现中提供了一个不太具有可用性的Observer。Browser从Server开始Load页面和Load页面结束的时候都有消息发布,实现消息处理的Listener理当也在UI线程中工作。但是Browser并没有提供Load结束之后DOM Document的Parse结束的消息。在Load结束的时候Document还没有进行parse。如果想用DOM Document,应该怎么去做呢?
在Listener的事件处理中等待是不可以的,因为这是UI线程,UI线程还在等待Listener的返回,才能继续执行DOM Document的Parse工作。所以,等待只能是浪费时间。
在Listener的事件处理中开新的线程,让新的线程等待。这个方案也是不可以的,因为UI线程的资源之后UI线程内部才能访问,普通线程访问UI资源只会抛出Invalid thread access异常。
我所能想到的实现是:
Display.getCurrent().timerExec(interval, new Runnable(){...});
这样就可以在Browser结束Load页面之后的一段时间内再次得到UI线程的一个时间片,检测DOM Document如果没有parse结束,再等下一个时间片,如果parse结束,就可以做一些处理了。
如果可以修改Mozilla XUL Browser源代码的话,可以添加一个Listener, Parser DOM Document结束之后的发布事件,这才是正解。所以说:Observer设计模式在设置消息发布的时机要设置的得当。