研磨设计模式 之 观察者模式(Observer) 3——跟着cc学设计系列
12.3 模式讲解
12.3.1 认识观察者模式
(1)目标和观察者之间的关系
按照模式的定义,目标和观察者之间是典型的一对多的关系。
但是要注意,如果观察者只有一个,也是可以的,这样就变相实现了目标和观察者之间一对一的关系,这也使得在处理一个对象的状态变化会影响到另一个对象的时候,也可以考虑使用观察者模式。
同样的,一个观察者也可以观察多个目标,如果观察者为多个目标定义的通知更新方法都是update方法的话,这会带来麻烦,因为需要接收多个目标的通知,如果是一个update的方法,那就需要在方法内部区分,到底这个更新的通知来自于哪一个目标,不同的目标有不同的后续操作。
一般情况下,观察者应该为不同的观察者目标,定义不同的回调方法,这样实现最简单,不需要在update方法内部进行区分。
(2)单向依赖
在观察者模式中,观察者和目标是单向依赖的,只有观察者依赖于目标,而目标是不会依赖于观察者的。
它们之间联系的主动权掌握在目标手中,只有目标知道什么时候需要通知观察者,在整个过程中,观察者始终是被动的,被动的等待目标的通知,等待目标传值给它。
对目标而言,所有的观察者都是一样的,目标会一视同仁的对待。当然也可以通过在目标里面进行控制,实现有区别对待观察者,比如某些状态变化,只需要通知部分观察者,但那是属于稍微变形的用法了,不属于标准的、原始的观察者模式了。
(3)基本的实现说明
具体的目标实现对象要能维护观察者的注册信息,最简单的实现方案就如同前面的例子那样,采用一个集合来保存观察者的注册信息。
具体的目标实现对象需要维护引起通知的状态,一般情况下是目标自身的状态,变形使用的情况下,也可以是别的对象的状态。
具体的观察者实现对象需要能接收目标的通知,能够接收目标传递的数据,或者是能够主动去获取目标的数据,并进行后续处理。
如果是一个观察者观察多个目标,那么在观察者的更新方法里面,需要去判断是来自哪一个目标的通知。一种简单的解决方案就是扩展update方法,比如在方法里面多传递一个参数进行区分等;还有一种更简单的方法,那就是干脆定义不同的回调方法。
(4)命名建议
观察者模式又被称为发布-订阅模式
目标接口的定义,建议在名称后面跟Subject
观察者接口的定义,建议在名称后面跟Observer
观察者接口的更新方法,建议名称为update,当然方法的参数可以根据需要定义,参数个数不限、参数类型不限
(5)触发通知的时机
在实现观察者模式的时候,一定要注意触发通知的时机,一般情况下,是在完成了状态维护后触发,因为通知会传递数据,不能够先通知后改数据,这很容易出问题,会导致观察者和目标对象的状态不一致。比如:目标一发出通知,就有观察者来取值,结果目标还没有更新数据,这就明显造成了错误。如下示例就是有问题的了,示例代码如下:
public void setContent(String content) {
//一激动,目标先发出通知了,然后才修改自己的数据,这会造成问题
notifyAllReader();
this.content = content;
}
(6)相互观察
在某些应用里面,可能会出现目标和观察者相互观察的情况。什么意思呢,比如有两套观察者模式的应用,其中一套观察者模式的实现是A对象、B对象观察C对象;在另一套观察者模式的实现里面,实现的是B对象、C对象观察A对象,那么A对象和C对象就是在相互观察。
换句话说,A对象的状态变化会引起C对象的联动操作,反过来,C 对象的状态变化也会引起A对象的联动操作。对于出现这种状况,要特别小心处理,因为可能会出现死循环的情况。
(7)观察者模式的调用顺序示意图
在使用观察者模式时,会很明显的分成两个阶段,第一个阶段是准备阶段,也就是维护目标和观察者关系的阶段,这个阶段的调用顺序如图12.5所示:
图12.5 观察者模式准备阶段示意图
接下来就是实际的运行阶段了,这个阶段的调用顺序如图12.6所示:
图12.6 观察者模式运行阶段示意图
(8)通知的顺序
从理论上说,当目标对象的状态变化后通知所有观察者的时候,顺序是不确定的,因此观察者实现的功能,绝对不要依赖于通知的顺序,也就是说,多个观察者之间的功能是平行的,相互不应该有先后的依赖关系。
12.3.2 推模型和拉模型
在观察者模式的实现里面,又分为推模型和拉模型两种方式,什么意思呢?
推模型
目标对象主动向观察者推送目标的详细信息,不管观察者是否需要,推送的信息通常是目标对象的全部或部分数据,相当于是在广播通信。
拉模型
目标对象在通知观察者的时候,只传递少量信息,如果观察者需要更具体的信息,由观察者主动到目标对象中获取,相当于是观察者从目标对象中拉数据。
一般这种模型的实现中,会把目标对象自身通过update方法传递给观察者,这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。
根据上面的描述,发现前面的例子就是典型的拉模型,那么推模型如何实现呢,还是来看个示例吧,这样会比较清楚。
(1)推模型的观察者接口
根据前面的讲述,推模型通常都是把需要传递的数据直接推送给观察者对象,所以观察者接口中的update方法的参数需要发生变化,示例代码如下:
/**
* 观察者,比如报纸的读者
*/
public interface Observer {
/**
* 被通知的方法,直接把报纸的内容推送过来
* @param content 报纸的内容
*/
public void update(String content);
}
(2)推模型的观察者的具体实现
以前需要到目标对象里面获取自己需要的数据,现在是直接接收传入的数据,这就是改变的地方,示例代码如下:
public class Reader implements Observer{
/**
* 读者的姓名
*/
private String name;
public void update(String content) {
//这是采用推的方式
System.out.println(name+"收到报纸了,阅读先。内容是==="
+content);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
(3)推模型的目标对象
跟拉模型的目标实现相比,有一些变化:
一个就是通知所有观察者的方法,以前是没有参数的,现在需要传入需要主动推送的数据
另外一个就是在循环通知观察者的时候,也就是循环调用观察者的update方法的时候,传入的参数不同了
示例代码如下:
/**
* 目标对象,作为被观察者,使用推模型
*/
public class Subject {
/**
* 用来保存注册的观察者对象,也就是报纸的订阅者
*/
private List<Observer> readers = new ArrayList<Observer>();
/**
* 报纸的读者需要先向报社订阅,先要注册
* @param reader 报纸的读者
* @return 是否注册成功
*/
public void attach(Observer reader) {
readers.add(reader);
}
/**
* 报纸的读者可以取消订阅
* @param reader 报纸的读者
* @return 是否取消成功
*/
public void detach(Observer reader) {
readers.remove(reader);
}
/**
* 当每期报纸印刷出来后,就要迅速的主动的被送到读者的手中,
* 相当于通知读者,让他们知道
* @param content 要主动推送的内容
*/
protected void notifyObservers(String content) {
for(Observer reader : readers){
reader.update(content);
}
}
}
(4)推模型的目标具体实现
跟拉模型相比,有一点变化,就是在调用通知观察者的方法的时候,需要传入参数了,拉模型的实现中是不需要的,示例代码如下:
public class NewsPaper extends Subject{
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
//内容有了,说明又出报纸了,那就通知所有的读者
notifyObservers(content);
}
}
(5)推模型的客户端使用
跟拉模型一样,没有变化。
好了,到此就简单的实现了拉模型的观察者模式,去测试一下,看看效果,是不是和前面的推模型一样呢?如果是一样的,那就对了。
(6)关于两种模型的比较
两种实现模型,在开发的时候,究竟应该使用哪一种,还是应该具体问题具体分析。这里,只是把两种模型进行一个简单的比较。
推模型是假定目标对象知道观察者需要的数据;而拉模型是目标对象不知道观察者具体需要什么数据,没有办法的情况下,干脆把自身传给观察者,让观察者自己去按需取值。
推模型可能会使得观察者对象难以复用,因为观察者定义的update方法是按需而定义的,可能无法兼顾没有考虑到的使用情况。这就意味着出现新情况的时候,就可能需要提供新的update方法,或者是干脆重新实现观察者。
而拉模型就不会造成这样的情况,因为拉模型下,update方法的参数是目标对象本身,这基本上是目标对象能传递的最大数据集合了,基本上可以适应各种情况的需要。
12.3.3 Java中的观察者模式
估计有些朋友在看前面的内容的时候,心里就嘀咕上了,Java里面不是已经有了观察者模式的部分实现吗,为何还要全部自己从头做呢?
主要是为了让大家更好的理解观察者模式本身,而不用受Java语言实现的限制。
好了,下面就来看看如何利用Java中已有的功能来实现观察者模式。在java.util包里面有一个类Observable,它实现了大部分我们需要的目标的功能;还有一个接口Observer,它里面定义了update的方法,就是观察者的接口。
因此,利用Java中已有的功能来实现观察者模式非常简单,跟前面完全由自己来实现观察者模式相比有如下改变:
不需要再定义观察者和目标的接口了,JDK帮忙定义了
具体的目标实现里面不需要再维护观察者的注册信息了,这个在Java中的Observable类里面,已经帮忙实现好了
触发通知的方式有一点变化,要先调用setChanged方法,这个是Java为了帮助实现更精确的触发控制而提供的功能
具体观察者的实现里面,update方法其实能同时支持推模型和拉模型,这个是Java在定义的时候,就已经考虑进去了
好了,说了这么多,还是看看例子会比较直观。
(1)新的目标的实现,不再需要自己来实现Subject定义,在具体实现的时候,也不是继承Subject了,而是改成继承Java中定义的Observable,示例代码如下:
/**
* 报纸对象,具体的目标实现
*/
public class NewsPaper extends java.util.Observable {
/**
* 报纸的具体内容
*/
private String content;
/**
* 获取报纸的具体内容
* @return 报纸的具体内容
*/
public String getContent() {
return content;
}
/**
* 示意,设置报纸的具体内容,相当于要出版报纸了
* @param content 报纸的具体内容
*/
public void setContent(String content) {
this.content = content;
//内容有了,说明又出新报纸了,那就通知所有的读者
//注意在用Java中的Observer模式的时候,下面这句话不可少
this.setChanged();
//然后主动通知,这里用的是推的方式
this.notifyObservers(this.content);
//如果用拉的方式,这么调用
//this.notifyObservers();
}
}
(2)再看看新的观察者的实现,不是实现自己定义的观察者接口,而是实现由Java提供的Observer接口,示例代码如下:
/**
* 真正的读者,为了简单就描述一下姓名
*/
public class Reader implements java.util.Observer {
/**
* 读者的姓名
*/
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void update(Observable o, Object obj) {
//这是采用推的方式
System.out.println(name
+"收到报纸了,阅读先。目标推过来的内容是==="+obj);
//这是获取拉的数据
System.out.println(name
+"收到报纸了,阅读先。主动到目标对象去拉的内容是==="
+((NewsPaper)o).getContent());
}
}
(3)客户端使用
客户端跟前面的写法没有太大改变,主要在注册阅读者的时候,调用的方法跟以前不一样了,示例代码如下:
public class Client {
public static void main(String[] args) {
//创建一个报纸,作为被观察者
NewsPaper subject = new NewsPaper();
//创建阅读者,也就是观察者
Reader reader1 = new Reader();
reader1.setName("张三");
Reader reader2 = new Reader();
reader2.setName("李四");
Reader reader3 = new Reader();
reader3.setName("王五");
//注册阅读者
subject.addObserver(reader1);
subject.addObserver(reader2);
subject.addObserver(reader3);
//要出报纸啦
subject.setContent("本期内容是观察者模式");
}
}
赶紧测试一下,运行运行,看看结果,运行结果如下所示:
王五收到报纸了,阅读先。目标推过来的内容是===本期内容是观察者模式
王五收到报纸了,阅读先。主动到目标对象去拉的内容是===本期内容是观察者模式
李四收到报纸了,阅读先。目标推过来的内容是===本期内容是观察者模式
李四收到报纸了,阅读先。主动到目标对象去拉的内容是===本期内容是观察者模式
张三收到报纸了,阅读先。目标推过来的内容是===本期内容是观察者模式
张三收到报纸了,阅读先。主动到目标对象去拉的内容是===本期内容是观察者模式
然后好好对比自己实现观察者模式和使用Java已有的功能来实现观察者模式,看看有什么不同,有什么相同,好好体会一下。
12.3.4 观察者模式的优缺点
l 观察者模式实现了观察者和目标之间的抽象耦合
原本目标对象在状态发生改变的时候,需要直接调用所有的观察者对象,但是抽象出观察者接口过后,目标和观察者就只是在抽象层面上耦合了,也就是说目标只是知道观察者接口,并不知道具体的观察者的类,从而实现目标类和具体的观察者类之间解耦。
l 观察者模式实现了动态联动
所谓联动,就是做一个操作会引起其它相关的操作。由于观察者模式对观察者注册实行管理,那就可以在运行期间,通过动态的控制注册的观察者,来控制某个动作的联动范围,从而实现动态联动。
l 观察者模式支持广播通信
由于目标发送通知给观察者是面向所有注册的观察者,所以每次目标通知的信息就要对所有注册的观察者进行广播。当然,也可以通过在目标上添加新的功能来限制广播的范围。
在广播通信的时候要注意一个问题,就是相互广播造成死循环的问题。比如A和B两个对象互为观察者和目标对象,A对象发生状态变化,然后A来广播信息,B对象接收到通知后,在处理过程中,使得B对象的状态也发生了改变,然后B来广播信息,然后A对象接到通知后,又触发广播信息……,如此A引起B变化,B又引起A变化,从而一直相互广播信息,就造成死循环了。
l 观察者模式可能会引起无谓的操作
由于观察者模式每次都是广播通信,不管观察者需不需要,每个观察者都会被调用update方法,如果观察者不需要执行相应处理,那么这次操作就浪费了。
其实浪费了还好,怕就怕引起了误更新,那就麻烦了,比如:本应该在执行这次状态更新前把某个观察者删除掉,这样通知的时候就没有这个观察者了,但是现在忘掉了,那么就会引起误操作。
12.3.5 思考观察者模式
1:观察者模式的本质
观察者模式的本质:触发联动。
当修改目标对象的状态的时候,就会触发相应的通知,然后会循环调用所有注册的观察者对象的相应方法,其实就相当于联动调用这些观察者的方法。
而且这个联动还是动态的,可以通过注册和取消注册来控制观察者,因而可以在程序运行期间,通过动态的控制观察者,来变相的实现添加和删除某些功能处理,这些功能就是观察者在update的时候执行的功能。
同时目标对象和观察者对象的解耦,又保证了无论观察者发生怎样的变化,目标对象总是能够正确地联动过来。
理解这个本质对我们非常有用,对于我们识别和使用观察者模式有非常重要的意义,尤其是在变形使用的时候,万变不离其宗。
2:何时选用观察者模式
建议在如下情况中,选用观察者模式:
当一个抽象模型有两个方面,其中一个方面的操作依赖于另一个方面的状态变化,那么就可以选用观察者模式,将这两者封装成观察者和目标对象,当目标对象变化的时候,依赖于它的观察者对象也会发生相应的变化。这样就把抽象模型的这两个方面分离开了,使得它们可以独立的改变和复用。
如果在更改一个对象的时候,需要同时连带改变其它的对象,而且不知道究竟应该有多少对象需要被连带改变,这种情况可以选用观察者模式,被更改的那一个对象很明显就相当于是目标对象,而需要连带修改的多个其它对象,就作为多个观察者对象了。
当一个对象必须通知其它的对象,但是你又希望这个对象和其它被它通知的对象是松散耦合的,也就是说这个对象其实不想知道具体被通知的对象,这种情况可以选用观察者模式,这个对象就相当于是目标对象,而被它通知的对象就是观察者对象了。