💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
第22章 观察者模式 22.1 韩非子身边的卧底是谁派来的 《孙子兵法》有云:“知彼知己,百战不殆;不知彼而知己,一胜一负;不知彼,不知己,每战必殆”,那怎么才能知己知彼呢?知己是很容易的,自己的军队嘛,很容易知根知底,那怎么知彼呢?安插间谍是个好办法,这是古今中外屡试不爽的方法,我们今天就来讲一个间谍的故事。 韩非子大家都应该记得吧,法家的代表人物,主张建立法制社会,实施重罚制度,真是非常有远见呀!看看现在社会在呼吁什么,建立法制化的社会,这在2000多年前就已经提出了。大家可能还不知道,法家还有一个非常重要的代表人物——李斯。李斯是秦国的丞相,最终被残忍车裂的那位,李斯和韩非子都是荀子的学生,李斯是师兄,韩非子是师弟,若干年后,李斯成为最强诸侯国秦国的上尉,致力于统一全国,于是安插了间谍到各个国家的重要人物的身边,以获取必要的信息,韩非子作为韩国的重量级人物,身边自然有不少间谍,韩非子做的事,李斯都了如指掌,那可是相隔千里!怎么做到的呢?间谍呀!我们先通过程序把这个过程展现一下,看看李斯是怎么监控韩非子的,先看两个主角的类图,如图22-1所示。 ![](https://box.kancloud.cn/2016-08-14_57b00367e5ec9.jpg) 图22-1 监控者和被监控者 仅有这两个对象还是不够的,我们要解决的是李斯是怎么监控韩非子的?创建一个后台线程一直处于运行状态,一旦发现韩非子在吃饭或者娱乐就触发事件?这是真实世界的翻版,安排了一个间谍,观察韩非子的生活起居,并上报给李斯,然后李斯再触发update事件,类图继续扩充,如图22-2所示。 ![](https://box.kancloud.cn/2016-08-14_57b003680595f.jpg) 图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("韩非子在娱乐");      } } ![](https://box.kancloud.cn/2016-08-14_57b003681c74c.jpg) 图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所示。 ![](https://box.kancloud.cn/2016-08-14_57b0036831cf6.jpg) 图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程序,因为你的业务都发生了变化。这就是观察者模式。