转载请标明出处:http://blog.csdn.net/u012250875/article/details/77747878
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);
}
该接口约定了观察者的行为。所有观察者需要在被观察对象发生变化时做出相应的反应,所做的具体反应就是实现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)该类中含有一个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);
}
}
看看版本一,很容易发现,该方法会出很多问题,首先调用了所有的观察者的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);
}
}
看看版本二,依旧有问题,如果出现并发时,各线程对changed变量的读写操作不安全,可能出现脏读因此产生重复update或者不能update的情况,因此需要进行修改,如下:
//版本三
public synchronized void notifyObservers(Object arg) {
clearChanged();
for (int i = 0; i<obs.size(); i++)
obs.get(i).update(this, arg);
}
看看版本三,不会出现并发造成的变量状态不一致带来的错误操作,但是想一想观察者数量较多时或者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);
}
看看版本四,发现还是存在问题虽然解决了耗,但依旧会有问题,多线程在同步块进行了同步,但是执行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);
}
源码果然很严谨!!!
5)上面说了系统对观察者模式的支持有这么多优点,但依旧不可避免以下几个缺点:
A. Observable是一个具体实现类,面向细节了,而未面向抽象
B. 使用Observable时需要使用继承,由于java的类单继承性,如果你的类已经继承了一个类,将不能继承Observable来实现观察者模式,并且由于setChanged和clearChanged方法都是protected的,所以你也不能通过组合来完成观察者模式
4.应用
观察者模式是很常用的模式,尤其在界面编程,比如android中的BaseAdapter,就使用了观察者模式,当数据源发生变化时,通知界面重新绘制。下面我们来利用jdk中提供的Observer和Observable来实现一个观察者模式的例子。
需求:
镇上来了一位小丑,为大家表演节目,所有观看的观众会根据小丑表演的精彩与否来做出相应的反应,比如表演的好就鼓掌喝彩,表演的不好就倒喝彩,表演完毕观众就退场。
分析:
这里涉及到小丑和观众,小丑的数量是1,而观众的数量是n,观众会对小丑的举动做出相应的反应。这里很符合观察者模式的场景,因此,我们需要创建一个小丑类Clown,作为被观察对象,因此需要继承Observable类,同时具有表演的行为,退场的行为;同时需要创建一个观众类Viewer,作为观察者,因此需要实现Observer接口,同时观众具有喝彩的行为,倒喝彩的行为,退场的行为,每个观众还对应一个座位号。
实现:
import java.util.Observable;
import java.util.Random;
/** * @author puyafeng * @desc 小丑类 */
public class Clown extends Observable {
/** 表演的精彩 */
public static final int PERFORM_GOOD = 0;
/** 表演的糟糕 */
public static final int PERFORM_BAD = 1;
/** 表演完毕 */
public static final int PERFORM_COMPLETE = 2;
/** * 表演 */
public void perform() {
System.out.println("**小丑开始表演**");
int random = new Random().nextInt(2);
//小丑表演状态是随机值,0表演的好,1表演的差
switch (random) {
case PERFORM_GOOD:
System.out.println("**小丑状态很好,表演的很精彩!**");
break;
case PERFORM_BAD:
System.out.println("**小丑状态不好,出了点篓子!**");
break;
}
setChanged();
notifyObservers(random);//表演好坏通过该参数传递到观众的update方法的第二个参数上
}
/** * 表演结束,小丑退场 */
public void exit() {
System.out.println("**表演结束,小丑退场!**");
setChanged();
notifyObservers(PERFORM_COMPLETE);//退场消息通过该参数传递到观众的update方法的第二个参数上
}
}
import java.util.Observable;
import java.util.Observer;
/** * @author puyf * @desc 观众类 */
public class Viewer implements Observer {
private int seatNo;
public Viewer(int seatNo) {
this.seatNo = seatNo;
}
@Override
public void update(Observable o, Object arg) {
Integer state = (Integer) arg;
switch (state) {
case Clown.PERFORM_GOOD:
applause();
break;
case Clown.PERFORM_BAD:
CheerBack();
break;
case Clown.PERFORM_COMPLETE:
exit();
break;
default:
break;
}
}
/** * 鼓掌 */
private void applause() {
System.out.println("座位号" + getSeatNo() + "的观众鼓掌了!");
}
/** * 倒喝彩 */
private void CheerBack() {
System.out.println("座位号" + getSeatNo() + "的观众发出了倒喝彩!");
}
/** * 退场 */
private void exit() {
System.out.println("座位号" + getSeatNo() + "的观众退场!");
}
public int getSeatNo() {
return seatNo;
}
}
/** * * @author puyf * @desc 测试类 */
public class Test {
public static void main(String[] args) {
//来了一个小丑
Clown clown = new Clown();
//观众入场了
for (int i = 0; i < 20; i++) {
Viewer v = new Viewer(i);
clown.addObserver(v);
System.out.println("座号为"+v.getSeatNo()+"的观众入座");
}
//小丑开始表演
clown.perform();
//小丑表演完毕,退场
clown.exit();
}
}
执行结果:
座号为0的观众入座
座号为1的观众入座
座号为2的观众入座
座号为3的观众入座
座号为4的观众入座
座号为5的观众入座
座号为6的观众入座
座号为7的观众入座
座号为8的观众入座
座号为9的观众入座
座号为10的观众入座
座号为11的观众入座
座号为12的观众入座
座号为13的观众入座
座号为14的观众入座
座号为15的观众入座
座号为16的观众入座
座号为17的观众入座
座号为18的观众入座
座号为19的观众入座
**小丑开始表演**
**小丑状态不好,出了点篓子!**
座位号19的观众发出了倒喝彩!
座位号18的观众发出了倒喝彩!
座位号17的观众发出了倒喝彩!
座位号16的观众发出了倒喝彩!
座位号15的观众发出了倒喝彩!
座位号14的观众发出了倒喝彩!
座位号13的观众发出了倒喝彩!
座位号12的观众发出了倒喝彩!
座位号11的观众发出了倒喝彩!
座位号10的观众发出了倒喝彩!
座位号9的观众发出了倒喝彩!
座位号8的观众发出了倒喝彩!
座位号7的观众发出了倒喝彩!
座位号6的观众发出了倒喝彩!
座位号5的观众发出了倒喝彩!
座位号4的观众发出了倒喝彩!
座位号3的观众发出了倒喝彩!
座位号2的观众发出了倒喝彩!
座位号1的观众发出了倒喝彩!
座位号0的观众发出了倒喝彩!
**表演结束,谢谢各位观看,请各位观众退场!**
座位号19的观众退场!
座位号18的观众退场!
座位号17的观众退场!
座位号16的观众退场!
座位号15的观众退场!
座位号14的观众退场!
座位号13的观众退场!
座位号12的观众退场!
座位号11的观众退场!
座位号10的观众退场!
座位号9的观众退场!
座位号8的观众退场!
座位号7的观众退场!
座位号6的观众退场!
座位号5的观众退场!
座位号4的观众退场!
座位号3的观众退场!
座位号2的观众退场!
座位号1的观众退场!
座位号0的观众退场!
当然上面的Clown类中的perform方法和exit方法中调用了notifyObservers(Object obj)方法,有时候是在业务逻辑中调用该方法来通知,比如去掉perform和exit中的notifyObservers,而在我们这里的Test类中的main()方法 来根据具体的业务逻辑来调用notifyObservers并传入参数。