自实现观察者模式(发布/订阅模式)的一个隐藏bug

观察者模式(发布(publish -订阅(Subscribe)模式、模型-视图(View)模式、源-收听者(Listener)模式或从属者模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统。

观察者模式(Observer)完美的将观察者和被观察的对象分离开。举个例子,用户界面可以作为一个观察者,业务数据是被观察者,用户界面观察业务数据的变化,发现数据变化后,就显示在界面上。面向对象设计的一个原则是:系统中的每个类将重点放在某一个功能上,而不是其他方面。一个对象只做一件事情,并且将他做好。观察者模式在模块之间划定了清晰的界限,提高了应用程序的可维护性和重用性。

观察者设计模式定义了对象间的一种一对多的依赖关系,以便一个对象的状态发生变化时,所有依赖于它的对象都得到通知并自动刷新。

实现流程可以简述为一个或多个观察者对目标的状态感兴趣,它们通过将自己依附在目标对象上以便注册所感兴趣的内容。目标状态发生改变并且观察者可能对这些改变感兴趣,就会发生一个通知消息,调用每个观察者的更新方法。当观察者不再对目标状态感兴趣时,它们可以简单地将自己从中分离

使用场景包括关联行为场景、事件多级触发场景、跨系统的消息交换场景,如消息队列、事件总线的处理机制等。

明显优点是解除耦合,让耦合的双方都依赖于抽象,从而使得各自的变换都不会影响另一边的变换。

而明显缺点是在应用观察者模式时需要考虑一下开发效率和运行效率的问题,程序中包括一个被观察者、多个观察者,开发、调试等内容会比较复杂,而且在Java中消息的通知一般是顺序执行,那么一个观察者卡顿,会影响整体的执行效率,在这种情况下,一般会采用异步实现。

一个简易示意图如下:(目标自己处理事件的注册、删除与调用)


典型UML结构如下:


如果把事件的注册、删除、调用单独抽离出来整理成全局统一处理,就是发布/订阅模式,基本示意图如下:


发布/订阅模式的UML类图没找到,估计是没有太大画的必要,主要实现就是把调度中心做成单例,实现主要的3个函数:add、delete、和 dispatch ,并且add、delete 中由原来的单参数改为双参数即可。

对于观察者模式中的主体的典型实现如下:

publicabstractclassSubject {
    private Vector<Observer> vector = newVector<Observer>();
 
    publicvoid addObserver(Observer observer){
        vector.add(observer);
    }
 
    publicvoid deleteObserver(Observer observer){
        vector.remove(observer);
    }
 
    publicvoid notifyAllObserver(){
        for(Observer observer:vector){
            observer.update(this);
        }
   }
}

其中除了可能重复添加的小问题(很容易看出,也很容易检查修正)外,还隐藏了一个同步的问题,以至于在很长的一段时间内,总会有莫名其妙的bug出现:存在已经注册的回调函数在事件发布时却没有被触发运行到,而第二次及以后事件再发布时就能触发运行到。

当时的一个临时办法是对于可能漏触发的函数注册两次,要删除时也删除两次。这在很大程度上算解决了该问题(黑盒测不出问题),但是对于以后的开发仍存在陷阱与不安全。

我们知道不能在循环体中对循环变量自身做长度上的变化,在上述的这个典型实现中其实就有这样的风险。在 notifyAllObserver函数的循环中,可能调用了一个方法 observer.Update observer.Update中存在一个删除该事件监听的逻辑(即只需相应一次) deleteObserverobserver),这样 vector 就发生了长度上的变化,丢失了正在循环的那一项,后续的项被提前一位,而循环仍继续。造成的影响就是,后续被提前的那一项的 observer.update不会被运行到。

解决方法也很简单,就是在循环前先把 vector循环变量浅拷贝一份再循环,这样即使修改 vector本身也不会漏了。

下面是Java 的内部实现源码参考:

/**
     * If this object has changed,as indicated by the
     *<code>hasChanged</code> method, then notify all of its observers
     * and then call the<code>clearChanged</code> method to indicate
     * that this object has nolonger changed.
     * <p>
     * Each observer has its<code>update</code> method called with two
     * arguments: this observableobject and the <code>arg</code> argument.
     *
     * @param   arg   any object.
     * @see    java.util.Observable#clearChanged()
     * @see    java.util.Observable#hasChanged()
     * @see    java.util.Observer#update(java.util.Observable, java.lang.Object)
     */
    publicvoid notifyObservers(Object arg) {
        /*
         * a temporary array buffer,used as a snapshot of the state of
         * current Observers.
         */
        Object[] arrLocal;
 
        synchronized (this) {
            /* We don't want the Observer doingcallbacks into
             * arbitrary code whileholding its own Monitor.
             * The code where weextract each Observable from
             * the Vector and storethe state of the Observer
             * needssynchronization, but notifying observers
             * does not (shouldnot).  The worst result of any
             * potentialrace-condition here is that:
             * 1) a newly-addedObserver will miss a
             *   notification in progress
             * 2) a recently unregisteredObserver will be
             *   wrongly notified when it doesn't care
             */
            if (!changed)
                return;
            arrLocal =obs.toArray();
            clearChanged();
        }
 
        for (int i = arrLocal.length-1; i>=0; i--)
           ((Observer)arrLocal[i]).update(this, arg);
}

它的解决方式是从后往前循环遍历,这样即使自己被删除也没影响。但是细想想也有可能在运行某个 update1时需要删除另一个 update2的监听,虽然这样写很不合规范,但是貌似仍是有风险的。所以在循环前浅拷贝,然后在循环体中判空才算稳妥。


参考:

百度百科:

https://baike.baidu.com/item/%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F/5881786?fr=aladdin

http://blog.csdn.net/sld880311/article/details/70224143


猜你喜欢

转载自blog.csdn.net/likkklikkk/article/details/76685724