JAVA观察者模式详解包含观察者模式JDK的漏洞以及事件驱动模型.doc
《JAVA观察者模式详解包含观察者模式JDK的漏洞以及事件驱动模型.doc》由会员分享,可在线阅读,更多相关《JAVA观察者模式详解包含观察者模式JDK的漏洞以及事件驱动模型.doc(18页珍藏版)》请在冰点文库上搜索。
![JAVA观察者模式详解包含观察者模式JDK的漏洞以及事件驱动模型.doc](https://file1.bingdoc.com/fileroot1/2023-4/30/e805d650-97e0-4232-8942-7d20a050233c/e805d650-97e0-4232-8942-7d20a050233c1.gif)
本章我们讨论一个除前面的单例以及代理模式之外,一个WEB项目中有可能用到的设计模式,即观察者模式。
说起观察者模式,LZ还是非常激动的,当初这算是第一个让LZ感受到设计模式强大的家伙。
当初LZ要做一个小型WEB项目,要上传给服务器文件,一个需求就是要显示上传进度,LZ就是用这个模式解决了当时的问题,那时LZ接触JAVA刚几个月,比葫芦画瓢的用了观察者模式。
现在谈及观察者模式,能用到的地方就相对较多了,通常意义上如果一个对象状态的改变需要通知很多对这个对象关注的一系列对象,就可以使用观察者模式。
下面LZ先给出观察者模式标准版的定义,引自百度百科。
定义:
观察者模式(有时又被称为发布-订阅模式、模型-视图模式、源-收听者模式或从属者模式)是软件设计模式的一种。
在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。
这通常透过呼叫各观察者所提供的方法来实现。
此种模式通常被用来实作事件处理系统。
上面的定义当中,主要有这样几个意思,首先是有一个目标的物件,通俗点讲就是一个类,它管理了所有依赖于它的观察者物件,或者通俗点说是观察者类,并在它自己状态发生变化时,主动发出通知。
简单点概括成通俗的话来说,就是一个类管理着所有依赖于它的观察者类,并且它状态变化时会主动给这些依赖它的类发出通知。
那么我们针对上面的描述给出观察者模式的类图,百度百科没有给出观察者模式的类图,这里LZ自己使用工具给各位画一个。
可以看到,我们的被观察者类Observable只关联了一个Observer的列表,然后在自己状态变化时,使用notifyObservers方法通知这些Observer,具体这些Observer都是什么,被观察者是不关心也不需要知道的。
上面就将观察者和被观察者二者的耦合度降到很低了,而我们具体的观察者是必须要知道自己观察的是谁,所以它依赖于被观察者。
下面LZ给写出一个很简单的观察者模式,来使用JAVA代码简单诠释一下上面的类图。
首先是观察者接口。
packagenet;
//这个接口是为了提供一个统一的观察者做出相应行为的方法
publicinterfaceObserver{
voidupdate(Observableo);
}
再者是具体的观察者。
packagenet;
publicclassConcreteObserver1implementsObserver{
publicvoidupdate(Observableo){
System.out.println("观察者1观察到"+o.getClass().getSimpleName()+"发生变化");
System.out.println("观察者1做出相应");
}
}
packagenet;
publicclassConcreteObserver2implementsObserver{
publicvoidupdate(Observableo){
System.out.println("观察者2观察到"+o.getClass().getSimpleName()+"发生变化");
System.out.println("观察者2做出相应");
}
}
下面是被观察者,它有一个观察者的列表,并且有一个通知所有观察者的方法,通知的方式就是调用观察者通用的接口行为update方法。
下面我们看它的代码。
packagenet;
importjava.util.ArrayList;
importjava.util.List;
publicclassObservable{
Listobservers=newArrayList();
publicvoidaddObserver(Observero){
observers.add(o);
}
publicvoidchanged(){
System.out.println("我是被观察者,我已经发生变化了");
notifyObservers();//通知观察自己的所有观察者
}
publicvoidnotifyObservers(){
for(Observerobserver:
observers){
observer.update(this);
}
}
}
这里面很简单,新增两个方法,一个是为了改变自己的同时通知观察者们,一个是为了给客户端一个添加观察者的公共接口。
下面我们使用客户端调用一下,看一下客户端如何操作。
packagenet;
publicclassClient{
publicstaticvoidmain(String[]args)throwsException{
Observableobservable=newObservable();
observable.addObserver(newConcreteObserver1());
observable.addObserver(newConcreteObserver2());
observable.changed();
}
}
运行结果如下。
可以看到我们在操作被观察者时,只要调用changed方法,观察者们就会做出相应的动作,而添加观察者这个行为算是准备阶段,将具体的观察者关联到被观察者上面去。
缪买网
下面LZ给出一个有实际意义的例子,比如我们经常看的小说网站,都有这样的功能,就是读者可以订阅作者,这当中就有明显的观察者模式案例,就是作者和读者。
他们的关系是一旦读者关注了一个作者,那么这个作者一旦有什么新书,就都要通知读者们,这明显是一个观察者模式的案例,所以我们可以使用观察者模式解决。
由于JDK中为了方便开发人员,已经写好了现成的观察者接口和被观察者类,下面LZ先给出JDK中现成的观察者和被观察者代码,外加自己的一点解释,来帮助一些读者对JDK中对观察者模式的支持熟悉一下。
先来观察者接口。
//观察者接口,每一个观察者都必须实现这个接口
publicinterfaceObserver{
//这个方法是观察者在观察对象产生变化时所做的响应动作,从中传入了观察的对象和一个预留参数
voidupdate(Observableo,Objectarg);
}
下面是被观察者类。
importjava.util.Vector;
//被观察者类
publicclassObservable{
//这是一个改变标识,来标记该被观察者有没有改变
privatebooleanchanged=false;
//持有一个观察者列表
privateVectorobs;
publicObservable(){
obs=newVector();
}
//添加观察者,添加时会去重
publicsynchronizedvoidaddObserver(Observero){
if(o==null)
thrownewNullPointerException();
if(!
obs.contains(o)){
obs.addElement(o);
}
}
//删除观察者
publicsynchronizedvoiddeleteObserver(Observero){
obs.removeElement(o);
}
//notifyObservers(Objectarg)的重载方法
publicvoidnotifyObservers(){
notifyObservers(null);
}
//通知所有观察者,被观察者改变了,你可以执行你的update方法了。
publicvoidnotifyObservers(Objectarg){
//一个临时的数组,用于并发访问被观察者时,留住观察者列表的当前状态,这种处理方式其实也算是一种设计模式,即备忘录模式。
Object[]arrLocal;
//注意这个同步块,它表示在获取观察者列表时,该对象是被锁定的
//也就是说,在我获取到观察者列表之前,不允许其他线程改变观察者列表
synchronized(this){
//如果没变化直接返回
if(!
changed)
return;
//这里将当前的观察者列表放入临时数组
arrLocal=obs.toArray();
//将改变标识重新置回未改变
clearChanged();
}
//注意这个for循环没有在同步块,此时已经释放了被观察者的锁,其他线程可以改变观察者列表
//但是这并不影响我们当前进行的操作,因为我们已经将观察者列表复制到临时数组
//在通知时我们只通知数组中的观察者,当前删除和添加观察者,都不会影响我们通知的对象
for(inti=arrLocal.length-1;i>=0;i--)
((Observer)arrLocal[i]).update(this,arg);
}
//删除所有观察者
publicsynchronizedvoiddeleteObservers(){
obs.removeAllElements();
}
//标识被观察者被改变过了
protectedsynchronizedvoidsetChanged(){
changed=true;
}
//标识被观察者没改变
protectedsynchronizedvoidclearChanged(){
changed=false;
}
//返回被观察者是否改变
publicsynchronizedbooleanhasChanged(){
returnchanged;
}
//返回观察者数量
publicsynchronizedintcountObservers(){
returnobs.size();
}
}
被观察者除了一点同步的地方需要特殊解释一下,其余的相信各位都能看明白各个方法的用途。
其实上述JDK的类是有漏洞的,或者说,在我们使用观察者模式时要注意一个问题,就是notifyObservers这个方法中的这一段代码。
for(inti=arrLocal.length-1;i>=0;i--)
((Observer)arrLocal[i]).update(this,arg);
在循环遍历观察者让观察者做出响应时,JDK没有去抓取update方法中的异常,所以假设在这过程中有一个update方法抛出了异常,那么剩下还未通知的观察者就全都通知不到了,所以LZ个人比较疑惑这样的用意(LZ无法想象JAVA类库的制造者没考虑到这个问题),是sun当时真的忘了考虑这一点,还是另有它意?
当然各位读者如果有自己的见解可以告知LZ,不过LZ认为,不管是sun如此做是别有用意,还是真的欠考虑,我们都要注意在update方法里一定要处理好异常,个人觉得JDK中比较保险的做法还是如下这样。
for(inti=arrLocal.length-1;i>=0;i--){
try{
((Observer)arrLocal[i]).update(this,arg);
}catch(Throwablee){e.printStackTrace();}
}
这样无论其中任何一个update是否成功都不会影响其余的观察者进行更新状态,我们自己比较保险的做法就是给update方法整个加上try块,或者确认不会发生运行时异常。
上面LZ和各位一起分析了JDK中观察者模式的源码,下面我们就拿上述小说网的例子,做一个DEMO。
首先要搞清楚在读者和作者之间是谁观察谁,很明显,应该是读者观察作者。
所以作者是被观察者,读者是观察者,除了这两个类之外,我们还需要额外添加一个管理器帮我们管理下作者的列表便于读者关注,于是一个观察者模式的DEMO就出现了。
如下,首先是读者类,LZ在各个类都加了点注释。
//读者类,要实现观察者接口
publicclassReaderimplementsObserver{
privateStringname;
publicReader(Stringname){
super();
this.name=name;
}
publicStringgetName(){
returnname;
}
//读者可以关注某一位作者,关注则代表把自己加到作者的观察者列表里
publicvoidsubscribe(StringwriterName){
WriterManager.getInstance().getWriter(writerName).addObserver(this);
}
//读者可以取消关注某一位作者,取消关注则代表把自己从作者的观察者列表里删除
publicvoidunsubscribe(StringwriterName){
WriterManager.getInstance().getWriter(writerName).deleteObserver(this);
}
//当关注的作者发表新小说时,会通知读者去看
publicvoidupdate(Observableo,Objectobj){
if(oinstanceofWriter){
Writerwriter=(Writer)o;
System.out.println(name+"知道"+writer.getName()+"发布了新书《"+writer.getLastNovel()+"》,非要去看!
");
}
}
}
下面是作者类。
//作者类,要继承自被观察者类
publicclassWriterextendsObservable{
privateStringname;//作者的名称
privateStringlastNovel;//记录作者最新发布的小说
publicWriter(Stringname){
super();
this.name=name;
WriterManager.getInstance().add(this);
}
//作者发布新小说了,要通知所有关注自己的读者
publicvoidaddNovel(Stringnovel){
System.out.println(name+"发布了新书《"+novel+"》!
");
lastNovel=novel;
setChanged();
notifyObservers();
}
publicStringgetLastNovel(){
returnlastNovel;
}
publicStringgetName(){
returnname;
}
}
然后我们还需要一个管理器帮我们管理这些作者。
如下。
importjava.util.HashMap;
importjava.util.Map;
//管理器,保持一份独有的作者列表
publicclassWriterManager{
privateMapwriterMap=newHashMap();
//添加作者
publicvoidadd(Writerwriter){
writerMap.put(writer.getName(),writer);
}
//根据作者姓名获取作者
publicWritergetWriter(Stringname){
returnwriterMap.get(name);
}
//单例
privateWriterManager(){}
publicstaticWriterManagergetInstance(){
returnWriterManagerInstance.instance;
}
privatestaticclassWriterManagerInstance{
privatestaticWriterManagerinstance=newWriterManager();
}
}
好了,这下我们的观察者模式就做好了,这个简单的DEMO可以支持读者关注作者,当作者发布新书时,读者会观察到这个事情,会产生相应的动作。
下面我们写个客户端调用一下。
//客户端调用
publicclassClient{
publicstaticvoidmain(String[]args){
//假设四个读者,两个作者
Readerr1=newReader("谢广坤");
Readerr2=newReader("赵四");
Readerr3=newReader("七哥");
Readerr4=newReader("刘能");
Writerw1=newWriter("谢大脚");
Writerw2=newWriter("王小蒙");
//四人关注了谢大脚
r1.subscribe("谢大脚");
r2.subscribe("谢大脚");
r3.subscribe("谢大脚");
r4.subscribe("谢大脚");
//七哥和刘能还关注了王小蒙
r3.subscribe("王小蒙");
r4.subscribe("王小蒙");
//作者发布新书就会通知关注的读者
//谢大脚写了设计模式
w1.addNovel("设计模式");
//王小蒙写了JAVA编程思想
w2.addNovel("JAVA编程思想");
//谢广坤取消关注谢大脚
r1.unsubscribe("谢大脚");
//谢大脚再写书将不会通知谢广坤
w1.addNovel("观察者模式");
}
}
看下我们得到的结果,就会发现,我们确实通知了读者它所关注的作者的动态,而且读者取消关注以后,作者的动态将不再通知该读者。
下面是运行结果。
我们使用观察者模式的用意是为了作者不再需要关心他发布新书时都要去通知谁,更重要的是他不需要关心他通知的是读者还是其它什么人,他只知道这个人是实现了观察者接口的,即我们的被观察者依赖的只是一个抽象的接口观察者接口,而不关心具体的观察者都有谁都是什么,比如以后要是游客也可以关注作者了,那么只要游客类实现观察者接口,那么一样可以将游客列入到作者的观察者列表中。
另外,我们让读者自己来选择自己关注的对象,这相当于被观察者将维护通知对象的职能转化给了观察者,这样做的好处是由于一个被观察者可能有N多观察者,所以让被观察者自己维护这个列表会很艰难,这就像一个老师被许多学生认识,那么是所有的学生都记住老师的名字简单,还是让老师记住N多学生的名字简单?
答案显而易见,让学生们都记住一个老师的名字是最简单的。
另外,观察者模式分离了观察者和被观察者二者的责任,这样让类之间各自维护自己的功能,专注于自己的功能,会提高系统的可维护性和可重用性。
观察者模式其实还有另外一种形态,就是事件驱动模型,LZ个人觉得这两种方式大体上其实是非常相似的,所以LZ决定一起引入事件驱动模型。
不过观察者更多的强调的是发布-订阅式的问题处理,而事件驱动则更多的注重于界面与数据模型之间的问题,两者还是有很多适用场景上的区别的,虽不能一概而论,但放在一起讨论还是很方便各位理解二者。
说到事件驱动,由于JAVA在桌面应用程序方面有很多欠缺,所以swing的使用其实并不是特别广泛,因为你不可能要求大多数人的机子上都安装了JDK,除非你是给特殊用户人群开发的应用程序,这些用户在你的可控范围内,那么swing或许可以派上用场。
考虑到学习JAVA或者使用JAVA的人群大部分都是在进行web开发,所以本次讨论事件驱动,采用web开发当中所用到的示例。
相信各位都知道tomcat,这是一个app服务器,在使用的过程中,或许经常会有人用到listener,即监听器这个概念。
那么其实这个就是一个事件驱动模型的应用。
比如我们的spring,我们在应用启动的时候要初始化我们的IOC容器,那么我们的做法就是加入一个listener,这样伴随着tomcat服务器的启动,spring的IOC容器就会跟着启动。
那么这个listener其实就是事件驱动模型中的监听器,它用来监听它所感兴趣的事,比如我们springIOC容器启动的监听器,就是实现的ServletContextListener这个接口,说明它对servletContext感兴趣,会监听servletContext的启动和销毁。
LZ不打算使用这个例子作为讲解,因为它的内部运作比较复杂,需要搬上来tomcat的源码,对于新手来说,这是个噩耗,所以我们将上述的例子改为事件驱动来实现。
也好让各位针对性的对比观察者模式和事件驱动模型。
首先事件驱动模型与观察者模式勉强的对应关系可以看成是,被观察者相当于事件源,观察者相当于监听器,事件源会产生事件,监听器监听事件。
所以这其中就搀和到四个类,事件源,事件,监听器以及具体的监听器。
JDK当中依然有现成的一套事件模型类库,其中监听器只是一个标识接口,因为它没有表达对具体对象感兴趣的意思,所以也无法定义监听的事件,只是为了统一,用来给特定的监听器继承。
它的源代码如下。
packagejava.util;
/**
*Atagginginterfacethatalleventlistenerinterfacesmustextend.
*@sinceJDK1.1
*/
publicinterfaceEventListener{
}
由于代码很短,所以LZ没有删减,当中标注了,所有的事件监听器都必须继承,这是一个标识接口。
上述的事件,JDK当中也有一个现成的类供继承,就是EventObject,这个类的源代码如下。
publicclassEventObjectimplementsjava.io.Serializable{
privatestaticfinallongserialVersionUID=5516075349620653480L;
/**
*TheobjectonwhichtheEventinitiallyoccurred.
*/
protectedtransientObjectsource;
/**
*ConstructsaprototypicalEvent.
*
*@paramsourceTheobjectonwhichtheEventinitiallyoccurred.
*@exceptionIllegalArgumentExceptionifsourceisnull.
*/
publicEventObject(Objectsource){
if(source==null)
thrownewIllegalArgumentException("nullsource");
this.source=source;
}
/**
*TheobjectonwhichtheEventinitiallyoccurred.
*
*@returnTheobjectonwhichtheEventinitiallyoccurred.
*/
publicObjectgetSource(){
returnsource;
}
/**
*ReturnsaStringrepresentationofthisEventObject.
*
*@returnAaStringrepresentationofthisEventObject.
*/
publicStringtoString(){
returngetClass(