浅谈Observer在Java中的实现

写在前面... 1

简单玩具... 1

[ 实现一 ]:... 1

基本规范... 2

[ 实现二 ] :. 3

高级功能... 4

[ 实现三 ] :. 6

其他 – UI线程中的Observer. 8

参考资料... 8

 

写在前面

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机制可以解决这个问题,而JavaEclipse架构里利用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());

              }

           });

       }

    }

       注意:SafeRunnableJava基础类库中的Runnable不同.前者只是隔离异常,后者目的却是开一个新线程。

       调用ListeneronSubjectChanged不需要开启新线程,因为任务不大的时候开了没有必要; 任务大的时候,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,在中间虑掉这个ViewListener,这个方案和MFC的解决方案本质上相同。

3.         MVC数据保护

MVC模式中Model提供了读取和修改的方法。读取主要提供给View,修改主要提供给Controller。最简单的Model可以以JavaBean为例。

有些Listener(即View)不应该修改Model,我们需要控制ViewModel的修改。

如果我们把Model传给listener,就没办法控制Listener是否修改。

Java没有const关键字,不能传递const型参数;如果只传递一个只提供getter方法的接口,Listener也有可能对它做强制转化,使用Model提供给Controller的方法。所以,需要另想办法来解决。

可以考虑类似ServletHttpRequest的数据保护方式,在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

在使用SWTUI库时,需要注意UI线程和普通线程的不同。UI线程从本质上讲就是有消息循环的线程,普通线程就是没有消息循环的线程。UI线程发布事件的处理也是在UI线程内部,这时可以使用UI线程中的资源。但是如果使用其他线程就不可以使用UI线程中的资源。

Observer设计模式在设置消息发布的时机要设置的得当。举一个反例:MozillaXULBrowser实现中提供了一个不太具有可用性的ObserverBrowserServer开始Load页面和Load页面结束的时候都有消息发布,实现消息处理的Listener理当也在UI线程中工作。但是Browser并没有提供Load结束之后DOM DocumentParse结束的消息。在Load结束的时候Document还没有进行parse。如果想用DOM Document,应该怎么去做呢?

Listener的事件处理中等待是不可以的,因为这是UI线程,UI线程还在等待Listener的返回,才能继续执行DOM DocumentParse工作。所以,等待只能是浪费时间。

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设计模式在设置消息发布的时机要设置的得当。

 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值