观察者模式

转自:【java】Observer和Observable详解

            观察者模式(从放弃到入门)

1.必要性

1.1 观察者模式是oo设计中经常用到的模式之一,大家在解决实际需求时,观察者模式往往都会用到,而javase中已经提供了Observer接口和Observable类让你简单快速的实现观察者模式,因此有必要去了解Observer和Observable;

2.观察者模式概述

2.1 角色:被观察对象,观察者

2.2 关系: 
1).被观察对象:观察者 = 1:n 
2).被观察对象状态发生变化,会通知所有观察者,观察者将做出相应的反应

2.3 详细说明:参见【设计模式】观察者模式

3.源码分析

3.1 Observer接口 
Observer为java.util包下的一个接口,源码如下:

public interface Observer {
    void update(Observable o, Object arg);
}
  • 1
  • 2
  • 3

该接口约定了观察者的行为。所有观察者需要在被观察对象发生变化时做出相应的反应,所做的具体反应就是实现Observer接口的update方法,实现update方法时你可以用到两个参数,一个参数的类型是Observable,另一个参数的类型是Object。当然如果完全由自己去实现一个观察者模式的方案,自己去设计Observer接口时,可能不会设计这两个参数。那为什么jdk设计该接口时规定接口中有这两个参数呢?那就是通用性。想想整个观察者模式有哪些类之间需要交互?使用该模式时牵扯三个类,一个是观察者,一个是被观察对象,一个是调用者(调用者可以是被观察对象本身调用,更多情况是一个具体的业务类),当前接口代表观察者,要与被观察对象交互,因此update方法需要持有被观察对象(Observable)的引用,第一参数产生了;如何与调用者通信,则是添加了类型为Object的参数(该参数是调用者调用Observable实例的notifyObservers(Object obj)方法时传入的,当然也可以不传);第一个参数可以说是为观察者提供了一种拉取数据的方式,update中的业务可以根据所需去拉去自己想要的被观察对象的信息(一般被观察对象中提供getter),第二个参数则是由调用者调用notifyObservers(Object obj)将一些信息推过来。通过这两个参数,观察者,被观察对象,调用者(调用通知刷新方法的可能是被观察对象本身,此时只存在观察者与被观察者两者)三者就联系起来了。

3.2 Observable类 
Observable同样是java.util包下的接口(理所当然的事),该类的成员变量和方法如下:

public class Observable {
    private boolean changed = false;
    private Vector<Observer> obs;
    public Observable(){};
    protected synchronized void setChanged(){};
    protected synchronized void clearChanged(){};
    public synchronized void addObserver(Observer o){};
    public synchronized void deleteObserver(Observer o) {};
    public synchronized void deleteObservers(){};
    public synchronized boolean hasChanged(){};
    public synchronized int countObservers(){};
    public void notifyObservers(){};
    public void notifyObservers(Object arg){};
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

先说成员变量: 
1)该类中含有一个boolean型的变量changed,代表是否发生改变了,Observable类只提供这个boolean值来表明是否发生变化,而不定义什么叫变化,因为每个业务中对变化的具体定义不一样,因此子类自己来判断是否变化;该变量既提供了一种抽象(变与不变),同时提供了一种观察者更新状态的可延迟加载,通过后面的notifyObservers方法分析可知观察者是否会调用update方法,依赖于changed变量,因此即使被观察者在逻辑上发生改变了,只要不调用setChanged,update是不会被调用的。如果我们在某些业务场景不需要频繁触发update,则可以适时调用setChanged方法来延迟刷新。

2)该类含有一个集合类Vector,该类泛型为Observer,主要用来存放所有观察自己的对象的引用,以便在更新时可以挨个遍历集合中的观察者,逐个调用update方法 
说明: 
1.8的jdk源码为Vector,有版本的源码是ArrayList的集合实现; 
Vector这个类和ArrayList的继承体系是一致,主要有两点不同,一是Vector是线程安全的,ArrayList不是线程安全的,Vector的操作依靠在方法上加了同步关键字来保证线程安全,与此同时ArrayList的性能是要好于Vector的;二是Vector和ArrayList扩容阀值不太一样,ArrayList较Vector更节省空间;

再来说说方法: 
1)操作changed变量的方法为setChanged(),clearChanged(),hasChanged();见名知意,第一个设置变化状态,第二清除变化状态,这两个的访问权限都是protected,表明这两个方法由子类去调用,由子类来告诉什么时候被观察者发生变化了,什么时候变化消失,而hasChanged()方法的访问权限是公有的,调用者可以使用该方法。三个方法都有同步关键字保证变量的读写操作线程安全。

2)操作Vector类型变量obs的方法为addObserver(Observer o), deleteObserver(Observer o), deleteObservers(),countObservers(),这四个方法分别实现了动态添加观察者,删除观察者,删除所有观察者,获取观察者数量。四个方法的访问权限都是公有的,这是提供给调用者的方法,让调用者来实时动态的控制哪些观察者来观察该被观察对象。

3)操作Vector型变量obs的四个方法都加有同步关键字,但是我们刚才分析成员属性Vector obs这个变量时,说Vector类型为线程安全的,而上述四个方法为什么还要加同步关键字呢,这是怎么回事?据我推测应该是程序员重构遗留问题吧,因为前面我说道,有历史版本的源码是使用的ArrayList来持有Observer的引用,而ArrayList不是线程安全的,所以上述四个操作结合的方法需要加上同步关键字来保证线程安全,而后来换成线程安全的Vector了,但这四个操作集合的方法依旧保留了同步关键字。

4)两个对外的方法notifyObservers(),notifyObservers(Object arg),该方法由调用者来操作,用来通知所有的观察者需要做更新操作了。

先不看源码,想想应该怎么做呢?通知观察者们进行刷新操作,不就是用for循环一个一个操作集合中的Observer调用update方法嘛,这还不简单,于是版本一产生:

//版本一
public void notifyObservers(Object arg) {
    if(changed){
        for (int i = 0; i<obs.size(); i++)
            obs.get(i).update(this, arg);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

看看版本一,很容易发现,该方法会出很多问题,首先调用了所有的观察者的update方法,但是没有清除被观察的变化状态,由于changed变量状态没有重置,因此,如果notifyObservers被多次调用,即使Observable没有再发生变化,所有观察者的update方法已经会被执行。因此需要进行修改,如下:

//版本二
public void notifyObservers(Object arg) {
    clearChanged();
    if(changed){
        for (int i = 0; i<obs.size(); i++)
            obs.get(i).update(this, arg);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

看看版本二,依旧有问题,如果出现并发时,各线程对changed变量的读写操作不安全,可能出现脏读因此产生重复update或者不能update的情况,因此需要进行修改,如下:

//版本三
public synchronized void notifyObservers(Object arg) {
    clearChanged();
    for (int i = 0; i<obs.size(); i++)
        obs.get(i).update(this, arg);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

看看版本三,不会出现并发造成的变量状态不一致带来的错误操作,但是想一想观察者数量较多时或者update方法执行时间较长时,被观察者变化后,notifyObservers的执行时间大大增加,呈线性增长,比如并发数是20,而此时有10个线程发生changed并且调用了notifyObservers方法,那么10个线程执行该方法将进入同步,粗略计算耗时为10*for循环执行时长,因此需要进行修改,我们只对changed变量的读写部分加锁,不会引起变量状态的不一致性,同时当同步块的代码执行完毕后,该线程可以先去执行耗时的for循环,修改如下:

//版本四
public void notifyObservers(Object arg) {
    synchronized (this) {
        if (!changed)
           return;
        clearChanged();
    }
    for (int i = 0; i<obs.size(); i++)
        obs.get(i).update(this, arg);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

看看版本四,发现还是存在问题虽然解决了耗,但依旧会有问题,多线程在同步块进行了同步,但是执行for循环的时候,由于调用者可能不断在增删观察者,假如A线程刚执行完i

//源码的方案
public void notifyObservers(Object arg) {
        Object[] arrLocal;
        synchronized (this) {
            if (!changed)
                return;
            arrLocal = obs.toArray();
            clearChanged();
        }

        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

源码果然很严谨!!!

5)上面说了系统对观察者模式的支持有这么多优点,但依旧不可避免以下几个缺点: 
A. Observable是一个具体实现类,面向细节了,而未面向抽象 
B. 使用Observable时需要使用继承,由于java的类单继承性,如果你的类已经继承了一个类,将不能继承Observable来实现观察者模式,并且由于setChanged和clearChanged方法都是protected的,所以你也不能通过组合来完成观察者模式

源码Observable , Observer 解析

public class Observable {
    private boolean changed = false;
    private Vector<Observer> obs;

    public Observable() {
        obs = new Vector<>();
    }

    public synchronized void addObserver(Observer o) {
        if (o == null)
            throw new NullPointerException();
        if (!obs.contains(o)) {
            obs.addElement(o);
        }
    }

    public synchronized void deleteObserver(Observer o) {
        obs.removeElement(o);
    }


    public void notifyObservers() {
        notifyObservers(null);
    }


    public void notifyObservers(Object arg) {

        Object[] arrLocal;

        synchronized (this) {
            if (!changed)
                return;
            arrLocal = obs.toArray();
            clearChanged();
        }

        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }


    public synchronized void deleteObservers() {
        obs.removeAllElements();
    }


    protected synchronized void setChanged() {
        changed = true;
    }


    protected synchronized void clearChanged() {
        changed = false;
    }


    public synchronized boolean hasChanged() {
        return changed;
    }


    public synchronized int countObservers() {
        return obs.size();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

为了节省篇幅,我狠心的把注释全删掉了,其实有很多注释的。

如果前面的内容都理解的童鞋,看这个代码也应该很好理解。套路一样,但是有一些点我们应该注意:

  1. 有 addObserver()deleteObserver()添加和删除观察者对象
  2. 代码中用一个 changed 关键字来表示是否有改变,在发送改变前我们需要先使用setChanged()将其设置为true。 在 notifyObservers()之后自动调用 clearChanged() 恢复 changed 为false。所以不需要我们自己来重置。
  3. 我们应该注意所有的方法都是 synchronized 的,而且存储观察者对象的集合类也是 Vector obs,所以整个 Observable 都是线程安全的。

还需要看一下 Observer 接口吗?

public interface Observer {
    void update(Observable o, Object arg);
}
  • 1
  • 2
  • 3

确实精简,只有一个 update 方法。看看 Obsererable 对象中的 notifyObservers 方法 调用的 update:

 ((Observer)arrLocal[i]).update(this, arg);
  • 1

对没错,就是这样调用的。

一些体会

感觉设计模式确实很有意思,但是上大学的时候,我们居然没有看这门课,我的天!!这个例子完整的结合了书本,自己的思考,以及Java的源码,感觉写下来我自己也受益匪浅。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值