1.写在前面
最近笔者真好看到了SpringBoot
的监听器,大概看了下监听器的源码,知道后台的逻辑是使用的观察者模式,于是我打算看下观察者模式,于是就有了这篇博客,这篇博客主要参考了《你的设计模式》
2.观察者模式
《孙子兵法》有云: “知彼知己,百战不殆;不知彼而知己,一胜一负;不知彼,不知己,每战必殆”,那怎么才能知己知彼呢?知己是很容易的,自己的军队嘛,很容易知道,那怎么知彼呢?安插间谍是很好的一个办法,我们今天就来讲一个间谍的故事。
韩非子大家都应该记得吧,法家的代表人物,主张建立法制社会,实施重罚制度,真是非常有远见呀,看看现在社会在呼吁什么,建立法制化的社会,在 2000 多年前就已经提出了。大家可能还不知道,法家还有一个非常重要的代表人物,李斯,对,就是李斯,秦国的丞相,最终被残忍的车裂的那位,李斯和韩非子都是荀子的学生,李斯是师兄,韩非子是师弟,若干年后,李斯成为最强诸侯秦国的上尉,致力于统一全国,于是安插了间谍到各个国家的重要人物的身边,以获取必要的信息,韩非子作为韩国的重量级人物,身边自然没少间谍了,韩非子早饭吃的什么,中午放了几个 P,晚上在做什么娱乐,李斯都了如指掌,那可是相隔千里!怎么做到的呢?间谍呀!
2.1方式一
好,我们先通过程序把这个过程展现一下,看看李斯是怎么监控韩非子,先看类图:
这个类图应该是程序员最容易想到得,你要监控,我就给你监控,正确呀,我们来看程序的实现,先看我们的主角韩非子的接口(类似于韩非子这样的人,被观察者角色):
package com.ys.step1;
public interface IHanFeiZi {
//韩非子也是人也要吃饭
void haveBreakFast();
//韩非子也是人也要娱乐
void haveFun();
//是否在吃饭
boolean isHaveBreakFast();
//是否在娱乐
boolean isHaveFun();
//设置是否在吃饭
void setHaveBreakFast(boolean haveBreakFast);
//设置是否在娱乐
void setHaveFun(boolean haveFun);
}
然后我们再来看IHanFeiZi
的实现类HanFeiZi
,具体的代码如下:
package com.ys.step1;
public class HanFeiZi implements IHanFeiZi {
private volatile boolean isHaveBreakFast = false;
private volatile boolean isHaveFun = false;
@Override
public void haveBreakFast() {
System.out.println("韩非开始吃饭....");
this.isHaveBreakFast = true;
}
@Override
public void haveFun() {
System.out.println("韩非开始娱乐...");
this.isHaveFun = true;
}
public boolean isHaveBreakFast() {
return isHaveBreakFast;
}
public void setHaveBreakFast(boolean haveBreakFast) {
isHaveBreakFast = haveBreakFast;
}
public boolean isHaveFun() {
return isHaveFun;
}
public void setHaveFun(boolean haveFun) {
isHaveFun = haveFun;
}
}
其中有两个 getter/setter 方法,这个就没有在类图中表示出来,比较简单,通过 isHaveBreakfast和 isHaveFun 这两个布尔型变量来判断韩非子是否在吃饭或者娱乐,韩非子是属于被观察者,那还有观察者李斯,我们来看李斯这类人的接口:
package com.ys.step1;
public interface ILiSi {
void update(String context);
}
李斯这类人比较简单,一发现自己观察的对象发生了变化,比如吃饭,娱乐了,自己立刻也要行动起来,那怎么行动呢?看实现类:
package com.ys.step1;
public class LiSi implements ILiSi {
@Override
public void update(String context) {
System.out.println("李斯:观察到韩非子活动,开始向老板汇报了...");
this.reportToQiShiHuang(context);
System.out.println("李斯:汇报完毕,秦老板赏给他两个萝卜吃吃...\n");
}
public void reportToQiShiHuang(String reportContext) {
System.out.println("李斯:报告,秦老板!韩非子有活动了--->" + reportContext);
}
}
韩非子是秦始皇非常崇拜的人物,甚至说过见韩非子一面死又何憾!不过,韩非子还真是被秦始皇干掉的,历史呀上演过太多这样的悲剧。这么重要的人物有活动,你李斯敢不向老大汇报?
两个重量级的人物都定义出来了,那我们就来看看要怎么监控,先写个监控程序:
package com.ys.step1;
public class Watch extends Thread{
private IHanFeiZi hanFeiZi;
private ILiSi liSi;
private String type;
public Watch(IHanFeiZi hanFeiZi, ILiSi liSi, String type) {
this.hanFeiZi = hanFeiZi;
this.liSi = liSi;
this.type = type;
}
@Override
public void run() {
while (true){
if (this.type.equals("breakFast")){
if (hanFeiZi.isHaveBreakFast()){
this.liSi.update("韩非在吃饭");
this.hanFeiZi.setHaveBreakFast(false);
}
}else {
if (hanFeiZi.isHaveFun()){
this.liSi.update("韩非在娱乐");
this.hanFeiZi.setHaveFun(false);
}
}
}
}
}
监控程序继承了 java.lang.Thread 类,可以同时启动多个线程进行监控,Java 的多线程机制还是比较简单的,继承 Thread 类,重写 run()方法,然后 new SubThread(),再然后 subThread.start()就可以启动一个线程了,我们继续往下看:
package com.ys.step1;
public class Client {
public static void main(String[] args) throws Exception{
HanFeiZi hanFeiZi = new HanFeiZi();
LiSi liSi = new LiSi();
//观察早餐
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 飙升,Idea 不响应状态?看到了?看到了你还不想为什么?!看看上面的程序,别的就不多说了,使用了一个 while(true)这样一个死循环来做监听,你要是用到项目中,你要多少硬件投入进来?你还让不让别人的程序也 run 起来?!一台服务器就跑你这一个程序就完事了,错,绝对的错!于是我们有了方式二。
2.2方式二
错误也看到了,我们必须要修改,这个没有办法应用到项目中去呀,而且这个程序根本就不是面向对象的程序,这完全是面向过程的(我写出这样的程序也不容易呀,安慰一下自己),不改不行,怎么修改呢?我们来想,既然韩非子一吃饭李斯就知道了,那我们为什么不把李斯这个类聚集到韩非子这里类上呢?说改就改,立马动手,我们来看修改后的类图:
类图非常简单,就是在 HanFeiZi 类中引用了 IliSi 这个接口,看我们程序代码怎么修改,IhanFeiZi接口完全没有修改,我们来看 HanFeiZi 这个实现类:
package com.ys.step2;
public class HanFeiZi implements IHanFeiZi {
private ILiSi liSi = new LiSi();
@Override
public void haveBreakFast() {
System.out.println("韩非开始吃饭....");
this.liSi.update("韩非在吃饭");
}
@Override
public void haveFun() {
System.out.println("韩非开始娱乐...");
this.liSi.update("韩非在娱乐");
}
}
韩非子 HanFeiZi 实现类就把接口的两个方法实现就可以了,在每个方法中调用 LiSi.update()方法,完成李斯观察韩非子任务,李斯的接口和实现类都没有任何改变,我们再来看看 Client 程序的变更:
package com.ys.step2;
public class Client {
public static void main(String[] args) {
//定义出韩非子
HanFeiZi hanFeiZi = new HanFeiZi();
//然后这里我们看看韩非子在干什么
hanFeiZi.haveBreakFast();
//韩非子娱乐了
hanFeiZi.haveFun();
}
}
李斯都不用在 Client 中定义了,非常简单,运行结果如下:
运行结果正确,效率也比较高,是不是应该乐呵乐呵了?大功告成了?稍等等,你想在战国争雄的时候,韩非子这么有名望(法家代表)、有实力(韩国的公子,他老爹参与过争夺韩国王位)的人,就只有秦国一个国家关心他吗?想想也不可能呀,肯定有一大帮的各国的类似李斯这样的人在看着他,监视着一举一动,但是看看我们的程序,你在 HanFeiZi 这个类中定义:
private ILiSi liSi = new LiSi();
一下子就敲死了,只有李斯才能观察到韩非子,这是不对的,也就是说韩非子的活动只通知了李斯一个人,这不可能;再者,李斯只观察韩非子的吃饭,娱乐吗?政治倾向不关心吗?思维倾向不关心吗?杀人放火不关心吗?也就说韩非子的一系列活动都要通知李斯,那可怎么办?于是我们有了方式三。
2.3方式三
要按照上面的例子,我们不是要修改疯掉了吗?这和开闭原则严重违背呀,我们的程序有问题,怎么修改,来看类图:
我们把接口名称修改了一下,这样显得更抽象化,Observable 是被观察者,就是类似韩非子这样的人,Observer 接口是观察者,类似李斯这样的,同时还有其他国家的比如王斯、刘斯等,在 Observable 接口中有三个比较重要的方法,分别是 addObserver 增加观察者,deleteObserver 删除观察者,notifyObservers通知所有的观察者,这是什么意思呢?我这里有一个信息,一个对象,我可以允许有多个对象来察看,你观察也成,我观察也成,只要是观察者就成,也就是说我的改变或动作执行,会通知其他的对象,看程序会更明白一点,先看 Observable 接口:
package com.ys.step3;
public interface Observable {
void addObserver(Observer observer);
void deleteObserver(Observer observer);
void notifyObservers(String context);
}
这是一个通用的被观察者接口,所有的被观察者都可以实现这个接口。再来看韩非子的实现类:
package com.ys.step3;
import java.util.ArrayList;
import java.util.List;
public class HanFeiZi implements Observable {
private List<Observer> observerList = new ArrayList<>();
@Override
public void addObserver(Observer observer) {
observerList.add(observer);
}
@Override
public void deleteObserver(Observer observer) {
observerList.remove(observer);
}
@Override
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("韩非子在娱乐");
}
}
再来看观察者接口 Observer.java:
package com.ys.step3;
public interface Observer {
void update(String context);
}
然后是三个很无耻的观察者,偷窥狂嘛:
package com.ys.step3;
public class LiSi implements Observer {
@Override
public void update(String context) {
System.out.println("李斯:观察到韩非子活动,开始向老板汇报了...");
this.reportToQiShiHuang(context);
System.out.println("李斯:汇报完毕,秦老板赏给他两个萝卜吃吃...\n");
}
public void reportToQiShiHuang(String reportContext) {
System.out.println("李斯:报告,秦老板!韩非子有活动了--->" + reportContext);
}
}
李斯是真有其人,以下两个观察者是杜撰出来的:
package com.ys.step3;
public class WangSi implements Observer {
//王斯,看到韩非子有活动,自己就受不了
@Override
public void update(String str){
System.out.println("王斯:观察到韩非子活动,自己也开始活动了...");
this.cry(str);
System.out.println("王斯:真真的哭死了...\n");
}
//一看李斯有活动,就哭,痛哭
private void cry(String context){
System.out.println("王斯:因为"+context+", ——所以我悲伤呀! ");
}
}
package com.ys.step3;
public class LiuSi implements Observer {
//刘斯,观察到韩非子活动后,自己也做一定得事情
@Override
public void update(String str) {
System.out.println("刘斯:观察到韩非子活动,开始动作了...");
this.happy(str);
System.out.println("刘斯:真被乐死了\n");
}
//一看韩非子有变化,他就快乐
private void happy(String context) {
System.out.println("刘斯:因为" + context + ",——所以我快乐呀! ");
}
}
所有的历史人物都在场了,那我们来看看这场历史闹剧是如何演绎的:
package com.ys.step3;
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 程序,因为你业务都发生了变化。
细心的你可能已经发现,HanFeiZi 这个实现类中应该抽象出一个父类,父类完全实现接口,HanFeiZi这个类只实现两个方法 haveBreakfast 和 haveFun 就可以了,是的,是的,确实是应该这样,于是有了方式四。
2.4方式四
我 们 打 开 JDK 的 帮 助 文 件 看 看 , 查 找 一 下 Observable 是 不 是 已 经 有 这 个 类 了 ? JDK 中 提 供 了 :java.util.Observable 实现类和 java.util.Observer 接口,也就是 说我们上面 写的那个例 子中 的Observable 接口可以改换成 java.util.Observale 实现类了,看如下类图:
是不是又简单了很多?那就对了!然后我们看一下我们程序的变更,先看 HanFeiZi 的实现类:
package com.ys.step4;
import java.util.Observable;
public class HanFeiZi extends Observable {
public void haveBreakFast(){
System.out.println("韩非子:开始吃饭了...");
//通知所有的观察者
super.setChanged();
super.notifyObservers("韩非子在吃饭");
}
public void haveFun() {
System.out.println("韩非子:开始娱乐...");
//通知所有的观察者
super.setChanged();
super.notifyObservers("韩非子在娱乐");
}
}
改变的不多,引入了一个 java.util.Observable 对象,删除了增加、删除观察者的方法,简单了很多,那我们再来看观察者的实现类:
package com.ys.step4;
import java.util.Observable;
import java.util.Observer;
public class LiSi implements Observer {
@Override
public void update(Observable observable, Object obj) {
System.out.println("李斯:观察到韩非子活动,开始向老板汇报了...");
this.reportToQiShiHuang(obj.toString());
System.out.println("李斯:汇报完毕,秦老板赏给他两个萝卜吃吃...\n");
}
public void reportToQiShiHuang(String reportContext) {
System.out.println("李斯:报告,秦老板!韩非子有活动了--->" + reportContext);
}
}
package com.ys.step4;
import java.util.Observable;
import java.util.Observer;
public class WangSi implements Observer {
//王斯,看到韩非子有活动,自己就受不了
@Override
public void update(Observable observable, Object obj) {
System.out.println("王斯:观察到韩非子活动,自己也开始活动了...");
this.cry(obj.toString());
System.out.println("王斯:真真的哭死了...\n");
}
//一看李斯有活动,就哭,痛哭
private void cry(String context){
System.out.println("王斯:因为"+context+", ——所以我悲伤呀! ");
}
}
package com.ys.step4;
import java.util.Observable;
import java.util.Observer;
public class LiuSi implements Observer {
//刘斯,观察到韩非子活动后,自己也做一定得事情
@Override
public void update(Observable observable, Object obj) {
System.out.println("刘斯:观察到韩非子活动,开始动作了...");
this.happy(obj.toString());
System.out.println("刘斯:真被乐死了\n");
}
//一看韩非子有变化,他就快乐
private void happy(String context) {
System.out.println("刘斯:因为" + context + ",——所以我快乐呀! ");
}
}
然后再来看 Client 程序:
package com.ys.step4;
import java.util.Observer;
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();
}
}
程序体内没有任何变更,只是引入了一个接口而已,运行结果如下:
运行结果一样,只是通知的先后顺序不同而已,程序已经简约到极致了。
以上讲解的就是观察者模式,这个模式的通用类图如下:
观察者模式在实际项目的应用中非常常见,比如你到 ATM 机器上取钱,多次输错密码,卡就会被 ATM吞掉,吞卡动作发生的时候,会触发哪些事件呢?第一摄像头连续快拍,第二,通知监控系统,吞卡发生;第三,初始化 ATM 机屏幕,返回最初状态,你不能因为就吞了一张卡,整个 ATM 都不能用了吧,一般前两个动作都是通过观察者模式来完成的。
那观察者模式在什么情况下使用呢?观察者可以实现消息的广播,一个消息可以触发多个事件,这是观察者模式非常重要的功能。使用观察者模式也有两个重点问题要解决:
广播链的问题。如果你做过数据库的触发器,你就应该知道有一个触发器链的问题,比如表 A 上写了一个触发器,内容是一个字段更新后更新表 B 的一条数据,而表 B 上也有个触发器,要更新表 C,表 C 也有触发器…,完蛋了,这个数据库基本上就毁掉了!我们的观察者模式也是一样的问题,一个观察者可以有双重身份,即使观察者,也是被观察者,这没什么问题呀,但是链一旦建立,这个逻辑就比较复杂,可维护性非常差,根据经验建议,在一个观察者模式中最多出现一个对象既是观察者也是被观察者,也就是说消息最多转发一次(传递两次),这还是比较好控制的。
异步处理问题。 EJB 是一个非常好的例子,被观察者发生动作了,观察者要做出回应,如果观察者比较多,而且处理时间比较长怎么办?那就用异步呗,异步处理就要考虑线程安全和队列的问题,这个大家有时间看看 Message Queue,就会有更深的了解。
3.写在最后
本篇博客主要介绍了观察者模式,因为我最近在看SpringBoot事件的监听器,所以特地跑过来看看观察者模式。