在没有互联网的时代里,报纸是人们获取信息的一个非常重要的途径,很多人会定一份或者几份报纸。报社会保存订阅者的信息,每当出了新报纸时,立即送到订阅者手里。订阅者在这种关系中处于被动的状态,它依赖于报社的行为,只有新报纸出版了,它才有可能阅读其中的内容。在互联网时代,报社可以把它的新闻放到网上,订阅者通过RSS订阅,每当有新的新闻就推送给订阅者。问题是:报社如何把新闻推送给所有的订阅者?进一步抽象描述这个问题:当一个对象的状态发生改变时,如何让依赖于它的所有对象得到通知,并进行处理呢?
观察者模式是这个问题的合理解决方案。观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。使用观察者模式的解决思路:首先,报社应该持有一个订阅者的列表,允许随时添加订阅者,同时也可以删除订阅者;其次,每当新闻产生时,报社会把新闻发给每个订阅者,这样的话订阅者也需要提供了一个接受新闻的方法;报社调用每个订阅者的接受新闻的方法,这样报社的任务就完成了,至于订阅者怎么处理这些新闻,那就与报社无关了。在观察者模式中,订阅者就是观察者,报社就是目标,它被观察者观察,这也是观察者模式名字的由来。
结构
观察者模式有如下几个角色:
Subject:目标对象抽象,目标对象一般提供对观察者注册和退订的维护;当目标的状态发生改变时,目标负责所有注册的,有效的观察者(在观察者模式的变形中,有效的观察者是很重要的);
ConcreteSubject:具体的目标实现对象,用来维护目标状态,当目标对象的状态发生改变时,通知所有注册的、有效的观察者,让观察者提供相应的处理;
Observer:定义观察者接口,提供目标通知时对应的更新方法,这个更新方法进行相应的业务处理,可以在这个方法里回调目标对象,以获取目标对象的数据;
ConcreteObserver:观察者的具体实现对象,用来接收目标的通知,并进行相应的后续处理,比如更新自身的状态以保持与目标的相应状态一致。
目标的示意性代码:
/**
* 被观察的目标,它应该提供如下功能:
* 提供对观察者的注册和退订
* 当目标状态发生改变时,通知所有的观察者
* @author cxy
*/
public interface Subject {
//注册观察者
public void attach(Observer observer);
//取消观察者
public void dettach(Observer oserver);
//状态发生改变,通知所有有效的观察者
public void notifyObservers();
}
/**
* 具体的目标,实现了Subject接口
*/
public class ConcreteSubject implements Subject {
//持有注册的观察者
private List<Observer> observers = new ArrayList<Observer>();
private String content;
@Override
public void attach(Observer observer) {
observers.add(observer);
}
@Override
public void dettach(Observer observer) {
if(observers.contains(observer))
observers.remove(observer);
}
@Override
public void notifyObservers() {
for(Observer observer : observers)
observer.update(this);//把自己提供给观察者,这是拉模型
}
//状态发生了改变
public void setContent(String content) {
this.content = content;
notifyObservers();
}
}
观察者的示意性代码:
/**
* 观察者接口,定义接收通知的处理方法
* @author cxy
*
*/
public interface Observer {
public void update(Subject subject);
}
public class ConcretObserver implements Observer {
private String state;
//接收通知的处理方法,一般需要更新自身的状态与目标的状态保持一致
public void update(Subject subject) {
state = ((concreteSubject)subject).xxx();
}
}
使用观察者模式解决新闻订阅问题,我们需要一个具体定义报社接口,它是目标对象的抽象,还应该有具体的报社,它是目标的具体实现;然后定义订阅者的接口,观察者对象的抽象,然后定义具体的订阅者,它是观察者的具体实现。
import java.util.ArrayList;
import java.util.List;
/**
* 报社接口,它应该提供如下功能:
* 提供对订阅者的注册和退订
* 当出版新报纸时,通知所有的订阅者
* @author cxy
*/
public interface NewsPaper {
//注册观察者
public void attach(Reader reader);
//取消观察者
public void dettach(Reader reader);
//状态发生改变,通知所有有效的观察者
public void notifyObservers();
}
/**
* 具体的报纸,实现NewsPaper接口
* @author cxy
*
*/
public class NewsPaperImp implements NewsPaper {
//持有注册的观察者
private List<Reader> readers = new ArrayList<Reader>();
private String content;
@Override
public void attach(Reader reader) {
readers.add(reader);
}
@Override
public void dettach(Reader reader) {
if(readers.contains(reader))
readers.remove(reader);
}
@Override
public void notifyObservers() {
for(Reader reader : readers)
reader.update(this);
}
//状态发生了改变
public void setContent(String content) {
this.content = content;
notifyObservers();
}
public String getContent() {
return this.content;
}
}
/**
* 订阅者接口,当报社出新报纸时,处理相关业务
*
*/
public interface Reader {
public void update(NewsPaper news);
}
/**
* 具体的订阅者,为了简单起见,仅仅是打印一下内容
*
*/
public class ReaderImp implements Reader {
private String name;
public ReaderImp(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//一般来说修改自己的状态和目标保持一直
public void update(NewsPaper news) {
System.out.println(name + "收到了报纸,本期内容是:" + ((NewsPaperImp)news).getContent());
}
}
演示代码:
public class Client {
public static void main(String[] args) {
NewsPaperImp news = new NewsPaperImp();
Reader one = new ReaderImp("张三");
Reader two = new ReaderImp("李四");
Reader three = new ReaderImp("王五");
//注册
news.attach(one);
news.attach(two);
news.attach(three);
news.setContent("观察者模式");
//退订
news.dettach(one);
news.setContent("有人退订");
}
}
目标与观察者之间的关系
目标与观察者之间是一对多的关系,如果只有一个观察者,这个时候就退化成了一对一的关系,这是一种特殊情况,也可以使用观察者模式。观察者和目标是单向的依赖,观察者依赖于目标,主动权完全掌握在目标手中,观察者只能被动的等待通知。
一个观察者其实是可以观察多于一个的目标,它可以接受来自多个目标的通知,在这种情况下,观察者应该为每一个目标定义不同的通知更新方法。如果仅仅定义一个update()方法,在update()方法内部区分不同的目标,这是不妥的,极不优雅,而且出错的概率很高。应该为不同的目标定义不同的处理方法,这样逻辑就比较清晰了。
public interface Observer {
//处理目标A的方法
public void updateForA(A a);
//处理目标B的方法
public void updateForB(B b);
//处理目标C的方法
public void updateForC(C c);
}
目标和观察者之间可能会相互观察,比如有两个类A和B,在一套逻辑下,A是目标,B是观察者;在另一套逻辑下,B是目标,A是观察者。这种情况有可能在实际的应用中出现,但是要特别小心处理死循环的情况:A的改变会引起B的变化,如果B的这种变化恰巧又能引起A的变化,那么就有可能进入死循环了。
有效的观察者
在一般的观察者模式中,一旦目标的状态发生了改变,它会一视同仁的对待所有的观察者,通知它们更新。但是在实际应用中,往往需要根据具体的情况去通知相应的观察者。比如对同一个事情,不同岗位上的人做出的反应是不同的,以公安局举例,一般的个人纠纷直接找民警就可以解决了,这个时候没必要通知局领导吧,但是如果发生了恶劣的连续绑架案件,那么就应该通知局领导了。在观察者模式中也是如此,有些状态的改变只需要通知一部分观察者,这个时候就应该在目标的通知方法里进行判断,以确定通知哪些通知者,我个人觉得这甚至都不能算是观察者模式的变形,这是普遍存在的问题。在这种情况下,需要通知的观察者就是有效观察者,必须通知这些观察者,其他的观察者就不应该通知它们,以免引起误更新。
//进行逻辑判断,确定是否需要通知
public void notifyObserver() {
for(Observer obs : observers) {
if(判断是否需要通知该观察者)
obs.update(this);
//其他处理
}
}
推模型和拉模型
推模型:目标知道观察者需要的数据,在通知观察者更新的时候,目标主动向观察者推送详细信息,不管观察者是否需要,推送的信息通常是目标对象的全部或者部分数据,相当于在广播通信。在新闻的例子中,目标知道观察者需要的就是新闻的详细信息,所以它可以在通知观察者更新时,直接把新闻信息作为参数传递过去;观察者获得新闻信息后,就可以直接使用了,如下所示:
//目标直接把新闻信息(content)作为参数传递给订阅者
@Override
public void notifyObservers() {
for(Reader reader : readers)
reader.update(content);
}
//订阅者可以直接使用新闻信息
public void update(String content) {
System.out.println(name + "收到了报纸,本期内容是:" + content);
}
拉模型:目标在通知观察者时,只传递少量的信息,如果观察者需要更多的信息,需要观察者主动去目标那里“拉”。一般情况下,目标把自身传递过去,观察者可以通过目标的引用来获取想要的信息了。如下所示:
//目标把自身传递过去
@Override
public void notifyObservers() {
for(Reader reader : readers)
reader.update(this);
}
//观察者需要什么数据,就通过目标引用主动去“拉”
public void update(NewsPaper news) {
System.out.println(name + "收到了报纸,本期内容是:" + ((NewsPaperImp)news).getContent());
}
在拉模型中,目标把自己传递出去了,这基本上是目标对象能传递的最大数据集合了,基本上可以适应各种情况的需要。
总结
观察者模式的本质是:触发联动。当目标对象发生改变时,就会出发相应的通知,然后调用观察者相应的方法。而且这个联动还是动态的,可以随时添加和删除观察者。一定要理解观察者模式的本质,设计模式不是从来就有,也不是条条框框,我愿意把它理解为一种经验,一种大致正确的解决思路。
观察者模式的优点
(1)实现了观察者和目标之间的抽象耦合。通过定义观察者接口,目标只知观察者接口,不知具体的观察者,这样具体的观察者就与目标之间解耦了。
(2)动态联动。
(3)广播通信,目标通知的信息要对所有有效的观察者进行广播。
观察者模式的缺点:可能会引起无谓的操作。每次目标都对所有的观察者发通知,不管这个观察者是否需要,这有可能引起观察者的误更新,所以一定要区分对待观察者!
使用场景:
(1)一个抽象模型有两个方面,其中一个方面的操作依赖于另一个方面的状态变化。
(2)在更新一个对象的时候,需要连带更新其他对象,而且还不知道到底有多少个这样的对象。
(3)一个对象必须通知其他对象,同时又想跟被通知的对象解耦,这个时候就可以使用观察者模式,通过抽象一个观察者接口,就可以不跟具体的被通知对象打交道了。
Java中的观察者模式
Java本身就包含了观察者模式,在java.util包里提供了观察者接口:Observer,它只有一个方法: void update(Observable o, Object arg);当目标发生变化时,会调用它的update方法,这跟上面自己实现的观察者接口是一致的。同时它还提供了一个具体的类Observable,它就是目标对象,摘取其源码一窥究竟:
public class Observable {
//保存目标的状态,标示目标是否改变
private boolean changed = false;
//保存观察者,竟然用Vector,真土
private Vector 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 synchronized void deleteObservers() {
obs.removeAllElements();
}
//如果目标发生了改变,需要调用此方法
//注意它是protected,专为子类而使用
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();
}
//通知所有观察者,可以看出它相当于notifyObservers(null)
public void notifyObservers() {
notifyObservers(null);
}
/**
* 在这里进行了同步,它获取了此对象的Monitor,在它执行这段代码时,会有两个问题:
* (1)在这时添加观察者是不行的,必须得等它执行完,也就是说一个新添加的观察者没有得到通知
* (2)在这时删除观察者也是不行的,必须得等它执行完,也就是说一个应该删除的观察者获得了通知
* 首先会判断是否发生了改变,如果发生了改变,则把所有的观察者读到一个数组里
* 并清楚改变标记,然后挨个通知所有的观察者
* 看了这段源码,感觉实现的真不咋的
* @param arg
*/
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);
}
}
关于Observable的行为上面都有注释,我们可以发现Java默认的是拉模型,如果使用notifyObservers(),它会把目标本身传递过去,如果使用notifyObservers(arg),它同样会把目标本身传递过去。有一点需要注意:Observable在通知观察者之前,会先检查changed的状态,所以子类在调用notifyObservers()方法之前,必须要先调用setChanged()方法。如果继承了这个类,那就不能继承其他类了,这是一个限制,可以考虑使用内部类来继承此类。只能说这代码实现还真挺土的,竟然还用Vector,竟然没用泛型。感觉完全没必要使用Java提供了默认实现。
转载请注明:喻红叶《Java与模式-观察者模式》