多伦多书苑

设计模式之禅

本书仅供会员,测试期间不限,赶快看。

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 observerList = new ArrayList();
//增加观察者

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("李斯:汇报完毕...
");
}
//汇报给秦始皇

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程序,因为你的业务都发生了变化。这就是观察者模式。

Author:

退出移动版