首页 » 设计模式之禅(第2版) » 设计模式之禅(第2版)全文在线阅读

《设计模式之禅(第2版)》第22章 观察者模式

关灯直达底部

22.1 韩非子身边的卧底是谁派来的

《孙子兵法》有云:“知彼知己,百战不殆;不知彼而知己,一胜一负;不知彼,不知己,每战必殆”,那怎么才能知己知彼呢?知己是很容易的,自己的军队嘛,很容易知根知底,那怎么知彼呢?安插间谍是个好办法,这是古今中外屡试不爽的方法,我们今天就来讲一个间谍的故事。

韩非子大家都应该记得吧,法家的代表人物,主张建立法制社会,实施重罚制度,真是非常有远见呀!看看现在社会在呼吁什么,建立法制化的社会,这在2000多年前就已经提出了。大家可能还不知道,法家还有一个非常重要的代表人物——李斯。李斯是秦国的丞相,最终被残忍车裂的那位,李斯和韩非子都是荀子的学生,李斯是师兄,韩非子是师弟,若干年后,李斯成为最强诸侯国秦国的上尉,致力于统一全国,于是安插了间谍到各个国家的重要人物的身边,以获取必要的信息,韩非子作为韩国的重量级人物,身边自然有不少间谍,韩非子做的事,李斯都了如指掌,那可是相隔千里!怎么做到的呢?间谍呀!我们先通过程序把这个过程展现一下,看看李斯是怎么监控韩非子的,先看两个主角的类图,如图22-1所示。

图22-1 监控者和被监控者

仅有这两个对象还是不够的,我们要解决的是李斯是怎么监控韩非子的?创建一个后台线程一直处于运行状态,一旦发现韩非子在吃饭或者娱乐就触发事件?这是真实世界的翻版,安排了一个间谍,观察韩非子的生活起居,并上报给李斯,然后李斯再触发update事件,类图继续扩充,如图22-2所示。

图22-2 通过后台线程监控

这个类图应该是程序员最容易想到的,你要监控,我就给你找个间谍角色(Spy类),我们来看程序的实现,先看我们的主角韩非子的接口(类似于韩非子这样的人,被观察者角色),如代码清单22-1所示。

代码清单22-1 被观察者接口

public interface IHanFeiZi {     //韩非子也是人,也要吃早饭的     public void haveBreakfast;     //韩非之也是人,是人就要娱乐活动     public void haveFun;}  

对接口进行扩充,增加了两个状态isHavingBreakfast(是否在吃早饭)和isHavingFun(是否在娱乐),以方便Spy进行监控,如代码清单22-2所示。

代码清单22-2 具体的被观察者

public class HanFeiZi implements IHanFeiZi{     //韩非子是否在吃饭,作为监控的判断标准     private boolean isHavingBreakfast = false;     //韩非子是否在娱乐     private boolean isHavingFun = false;     //韩非子要吃饭了     public void haveBreakfast{     System.out.println(/"韩非子:开始吃饭了.../");     this.isHavingBreakfast =true;     }     //韩非子开始娱乐了     public void haveFun{     System.out.println(/"韩非子:开始娱乐了.../");     this.isHavingFun = true;     }     //以下是bean的基本方法,getter/setter,不多说     public boolean isHavingBreakfast {     return isHavingBreakfast;     }     public void setHavingBreakfast(boolean isHavingBreakfast) {     this.isHavingBreakfast = isHavingBreakfast;     }     public boolean isHavingFun {     return isHavingFun;     }     public void setHavingFun(boolean isHavingFun) {     this.isHavingFun = isHavingFun;     }}  

其中有两个getter/setter方法,这个就没有在类图中表示出来,比较简单,通过isHavingBreakfast和isHavingFun这两个布尔型变量来判断韩非子是否在吃饭或者娱乐,韩非子属于被观察者,那还有观察者李斯,我们来看李斯的接口,如代码清单22-3所示。

代码清单22-3 抽象观察者

public interface ILiSi {     //一发现别人有动静,自己也要行动起来     public void update(String context);}  

李斯这类人比较简单,一发现自己观察的对象发生了变化,比如吃饭、娱乐,自己立刻也要行动起来,怎么行动呢?如代码清单22-4所示。

代码清单22-4 韩非子

public class LiSi implements ILiSi{     //首先李斯是个观察者,一旦韩非子有活动,他就知道,他就要向老板汇报     public void update(String str){     System.out.println(/"李斯:观察到韩非子活动,开始向老板汇报了.../");     this.reportToQinShiHuang(str);     System.out.println(/"李斯:汇报完毕...n/");     }     //汇报给秦始皇     private void reportToQinShiHuang(String reportContext){     System.out.println(/"李斯:报告,秦老板!韩非子有活动了--->/"+reportContext);     }}  

两个重量级的人物都定义出来了,间谍这个“卑鄙”小人是不是也要登台了,如代码清单22-5所示。

代码清单22-5 间谍

class Spy extends Thread{     private HanFeiZi hanFeiZi;     private LiSi liSi;     private String type;     //通过构造函数传递参数,我要监控的是谁,谁来监控,要监控什么     public Spy(HanFeiZi _hanFeiZi,LiSi _liSi,String _type){     this.hanFeiZi =_hanFeiZi;     this.liSi = _liSi;     this.type = _type;     }     @Override     public void run{     while(true){  if(this.type.equals(/"breakfast/")){ //监控是否在吃早餐  //如果发现韩非子在吃饭,就通知李斯  if(this.hanFeiZi.isHavingBreakfast){       this.liSi.update(/"韩非子在吃饭/");       //重置状态,继续监控       this.hanFeiZi.setHavingBreakfast(false);  }        }else{//监控是否在娱乐  if(this.hanFeiZi.isHavingFun){       this.liSi.update(/"韩非子在娱乐/");       this.hanFeiZi.setHavingFun(false);  }     }     }     }}  

监控程序继承了java.lang.Thread类,可以同时启动多个线程进行监控,Java的多线程机制还是比较简单的,继承Thread类,重写run方法,然后new SubThread,再然后subThread.start就可以启动一个线程了。我们建立一个场景类来回顾一下这段历史,如代码清单22-6所示。

代码清单22-6 场景类

public class Client {     public static void main(String args) throws InterruptedException {     //定义出韩非子和李斯     LiSi liSi = new LiSi;     HanFeiZi hanFeiZi = new HanFeiZi;     //观察早餐     Watch watchBreakfast = new Watch(hanFeiZi,liSi,/"breakfast/");     //开始启动线程,监控     watchBreakfast.start;     //观察娱乐情况     Watch watchFun = new Watch(hanFeiZi,liSi,/"fun/");     watchFun.start;     //然后我们看看韩非子在干什么     Thread.sleep(1000); //主线程等待1秒后后再往下执行     hanFeiZi.haveBreakfast;     //韩非子娱乐了     Thread.sleep(1000);     hanFeiZi.haveFun;     }}  

运行结果如下所示:

韩非子:开始吃饭了...

李斯:观察到韩非子活动,开始向老板汇报了...

李斯:报告,秦老板!韩非子有活动了--->韩非子在吃饭

李斯:汇报完毕

韩非子:开始娱乐了...

李斯:观察到韩非子活动,开始向老板汇报了...

李斯:报告,秦老板!韩非子有活动了--->韩非子在娱乐

李斯:汇报完毕

结果出来,韩非子一吃早饭李斯就知道,韩非子一娱乐李斯也知道,非常正确!结果正确但并不表示你有成绩,我告诉你:你的成绩是0,甚至是负的!你有没有看到你的CPU飙升,Eclipse不响应状态?看到了?看到了你还不想为什么?!看看上面的程序,别的就不多说了,使用了一个死循环while(true)来做监听,要是用到项目中,你要多少硬件投入进来?你还让不让别人的程序运行了?!一台服务器就跑你这一个程序就完事!

错误也看到了,我们必须要修改,这个没法应用到项目中,而且这个程序根本就不是面向对象的程序,这完全是面向过程的,不改不行,怎么修改呢?我们来想,既然韩非子一吃饭李斯就知道了,那我们为什么不把李斯这个类聚集到韩非子那个类上呢?说改就改,立马动手,我们来看修改后的类图,如图22-3所示。

类图非常简单,就是在HanFeiZi类中引用了LiSi实例,看我们程序代码怎么修改,IHanFeiZi接口完全没有修改,可以参考代码清单22-1所示。我们来看实现类的修改,如代码清单22-7所示。

代码清单22-7 通过聚集方式的被观察者

public class HanFeiZi implements IHanFeiZi{     //把李斯声明出来     private ILiSi liSi =new LiSi;     //韩非子要吃饭了     public void haveBreakfast{     System.out.println(/"韩非子:开始吃饭了.../");     //通知李斯     this.liSi.update(/"韩非子在吃饭/");     }     //韩非子开始娱乐了     public void haveFun{     System.out.println(/"韩非子:开始娱乐了.../");     this.liSi.update(/"韩非子在娱乐/");     }}  

图22-3 通过聚集方式监控

韩非子HanFeiZi实现类就把接口的两个方法实现就可以了,在每个方法中调用LiSi.update方法,完成李斯观察韩非子的职责,李斯的接口和实现类都没有任何改变,请参考代码清单22-3、22-4。我们再来看看Client程序的变更,如代码清单22-8所示。

代码清单22-8 通过聚集方式的场景类

public class Client {     public static void main(String args) {     //定义出韩非子     HanFeiZi hanFeiZi = new HanFeiZi;     //然后我们看看韩非子在干什么     hanFeiZi.haveBreakfast;     //韩非子娱乐了     hanFeiZi.haveFun;     }}  

李斯就不用在场景类中定义了,非常简单,运行结果相同,不再赘述。

我们思考一下,修改后的程序运行结果正确,效率也比较高,是不是应该乐呵乐呵了?大功告成了?稍等等,你想在战国争雄的时候,韩非子这么有名望、有实力的人,就只有秦国关心他吗?想想也不可能呀,确实有一大帮的各国类似于李斯这样的人在看着他,监视着他的一举一动,但是看看我们的程序,你在HanFeiZi这个类中定义:

private ILiSi liSi =new LiSi;

这样一来只有李斯才能观察到韩非子,这是不对的,也就是说韩非子的活动只通知了李斯一个人,这不可能;再者说了,李斯只观察韩非子的吃饭、娱乐吗?政治倾向不关心吗?思维倾向不关心吗?杀人放火不关心吗?也就说韩非子的一系列活动都要通知李斯,这可怎么办?要按照上面的例子,我们如何修改?这和开闭原则严重违背呀,我们的程序有问题,修改如图22-4所示。

图22-4 改进后的观察者和被观察者

我们把原有类图做了两个修改:

● 增加Observable

实现该接口的都是被观察者,那韩非子是被观察者,他当然也要实现该接口了,同时他还有与其他庸人相异的事要做,因此他还是要实现IHanFeizi接口。

● 修改ILiSI接口名称为Observer

接口名称修改了一下,这样显得更抽象化,所有实现该接口的都是观察者(类似李斯这样的)。

Observable是被观察者,就是类似韩非子这样的人,在Observable接口中有三个比较重要的方法,分别是addObserver增加观察者,deleteObserver删除观察者,notifyObservers通知所有的观察者,这是什么意思呢?我这里有一个信息,一个对象,我可以允许有多个对象来察看,你观察也成,我观察也成,只要是观察者就成,也就是说我的改变或动作执行,会通知其他的对象,看程序会更明白一点,先看Observable接口,如代码清单22-9所示。

代码清单22-9 被观察者接口

public interface Observable {     //增加一个观察者     public void addObserver(Observer observer);     //删除一个观察者     public void deleteObserver(Observer observer);     //既然要观察,我发生改变了他也应该有所动作,通知观察者     public void notifyObservers(String context);}  

这是一个通用的被观察者接口,所有的被观察者都可以实现这个接口。再来看韩非子的实现类,如代码清单22-10所示。

代码清单22-10 被观察者实现类

public class HanFeiZi implements Observable ,IHanFeiZi{     //定义个变长数组,存放所有的观察者     private ArrayList<Observer> observerList = new ArrayList<Observer>;     //增加观察者     public void addObserver(Observer observer){     this.observerList.add(observer);     }     //删除观察者     public void deleteObserver(Observer observer){     this.observerList.remove(observer);     }     //通知所有的观察者     public void notifyObservers(String context){     for(Observer observer:observerList){     observer.update(context);     }     }     //韩非子要吃饭了     public void haveBreakfast{     System.out.println(/"韩非子:开始吃饭了.../");     //通知所有的观察者     this.notifyObservers(/"韩非子在吃饭/");     }     //韩非子开始娱乐了     public void haveFun{     System.out.println(/"韩非子:开始娱乐了.../");     this.notifyObservers(/"韩非子在娱乐/");     }}  

观察者只是把原有的ILiSi接口修改了一个名字而已,如代码清单22-11所示。

代码清单22-11 观察者接口

public interface Observer {     //一发现别人有动静,自己也要行动起来     public void update(String context);}  

然后是三个很无耻的观察者,咱先看看真实的李斯,如代码清单22-12所示。

代码清单22-12 具体的观察者

public class LiSi implements Observer{     //首先李斯是个观察者,一旦韩非子有活动,他就知道,他就要向老板汇报     public void update(String str){     System.out.println(/"李斯:观察到韩非子活动,开始向老板汇报了.../");     this.reportToQinShiHuang(str);     System.out.println(/"李斯:汇报完毕...n/");     }     //汇报给秦始皇     private void reportToQinShiHuang(String reportContext){     System.out.println(/"李斯:报告,秦老板!韩非子有活动了-->/"+reportContext);     }}  

李斯是真有其人,以下两个观察者王斯和刘斯是杜撰出来的,如代码清单22-13所示。

代码清单22-13 杜撰的观察者

public class WangSi implements Observer{     //王斯,看到韩非子有活动     public void update(String str){     System.out.println(/"王斯:观察到韩非子活动,自己也开始活动了.../");     this.cry(str);     System.out.println(/"王斯:哭死了...n/");     }     //一看韩非子有活动,他就痛哭     private void cry(String context){     System.out.println(/"王斯:因为/"+context+/",--所以我悲伤呀!/");     }}public class LiuSi implements Observer{//刘斯,观察到韩非子活动后,自己也得做一些事     public void update(String str){     System.out.println(/"刘斯:观察到韩非子活动,开始动作了.../");     this.happy(str);     System.out.println(/"刘斯:乐死了n/");     }     //一看韩非子有变化,他就快乐     private void happy(String context){     System.out.println(/"刘斯:因为/" +context+/",--所以我快乐呀!/" );     }} 

所有的历史人物都在场了,那我们来看看这场历史闹剧是如何演绎的,如代码清单22-14所示。

代码清单22-14 场景类

public class Client {     public static void main(String args) {     //三个观察者产生出来     Observer liSi = new LiSi;     Observer wangSi = new WangSi;     Observer liuSi = new LiuSi;//定义出韩非子     HanFeiZi hanFeiZi = new HanFeiZi;     //我们后人根据历史,描述这个场景,有三个人在观察韩非子     hanFeiZi.addObserver(liSi);     hanFeiZi.addObserver(wangSi);     hanFeiZi.addObserver(liuSi);//然后这里我们看看韩非子在干什么     hanFeiZi.haveBreakfast;     }}  

运行结果如下所示:

韩非子:开始吃饭了...

李斯:观察到韩非子活动,开始向老板汇报了...

李斯:报告,秦老板!韩非子有活动了--->韩非子在吃饭

李斯:汇报完毕...

王斯:观察到韩非子活动,自己也开始活动了...

王斯:因为韩非子在吃饭——所以我悲伤呀!

王斯:哭死了...

刘斯:观察到韩非子活动,开始动作了...

刘斯:因为韩非子在吃饭——所以我快乐呀!

刘斯:乐死了

好了,结果也正确了,也符合开闭原则了,同时也实现类间解耦,想再加观察者?继续实现Observer接口就成了,这时候必须修改Client程序,因为你的业务都发生了变化。这就是观察者模式。