本篇文章总结于马士兵的视频教程《观察者模式》。个人非常推荐马士兵的视频教程,对于初学Java的人来说,J2SE基础视频非常不错,对于内存分析讲的十分到位。对于有一定基础的人来说,设计模式系列,反射系列,正则表达式系列都非常不错,不仅仅局限于Java,而C#学习者也可以看一看。
这里通过一个生活例子来讲观察者模式。这个场景就是,有一个小孩在睡觉,然后他老爸在旁边,小孩醒了他老爸就要喂他吃东西。从面相对象的角度来讲,我们通过描述就可以抽象出实体类,小孩,老爸是两个对象,然后睡觉是小孩的状态,喂东西是老爸的方法。
第一种方法,我们用最直接的方式去模拟这个过程。当小孩在睡觉的时候,老爸会一直的去查看小孩是否醒了,1秒钟查看一次,模拟到程序里边,就是一种死循环,以轮循的方式去判断小孩的属性。这里把小孩设计成一个线程,最初是睡着状态,10秒后醒来。那么10秒后老爸就要调用其feed方法。小孩的toString()方法输出这个对象的唯一标识,表示这个特定的小孩。
package com.robin.test;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
Baby b = new Baby();
Dad d = new Dad(b);
new Thread(d).start();
new Thread(b).start();
System.in.read();
}
}
class Baby implements Runnable {
private boolean wakeUp = false;
public boolean isWakeUp() {
return wakeUp;
}
void wakeUp() {
wakeUp = true;
}
@Override
public void run() {
try {
Thread.sleep(1000 * 10);
this.wakeUp();
}
catch (Exception ex) {
ex.printStackTrace();
}
}
}
class Dad implements Runnable {
Baby baby;
private void feedBaby() {
System.out.println("Feed the baby..." + baby.toString());
}
public Dad(Baby baby) {
this.baby = baby;
}
@Override
public void run() {
while(true)
{
if(baby.isWakeUp() == true) {
feedBaby();
}
}
}
}
这种方式存在一定的弊端,即老爸得时时刻刻盯着小孩,这个过程中不能干别的事情,即使客厅里有足球赛,这时候老爸也不能把小孩放在房间里自己去客厅。反应到程序中来讲,这种死循环的方式,对于CPU是一种无端的消耗。
接下来我们用第二种方式进行模拟。我们可以这样,我们让小孩持有老爸的引用,当小孩醒了之后,主动调用老爸的feed方法。就相当于在小孩与老爸之间绑了一根绳子,小孩在里屋睡觉,老爸在客厅看球,当小孩醒了之后,拽动这个绳子,那么老爸得知消息后再进里屋来喂东西。代码可以如下。
package com.robin.test;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
Dad d = new Dad();
Baby b = new Baby(d);
new Thread(b).start();
System.in.read();
}
}
class Baby implements Runnable {
private boolean wakeUp = false;
private Dad dad;
public boolean isWakeUp() {
return wakeUp;
}
void wakeUp() {
wakeUp = true;
}
public Baby(Dad dad) {
this.dad = dad;
}
@Override
public void run() {
try {
Thread.sleep(1000 * 10);
this.wakeUp();
dad.feedBaby();
}
catch (Exception ex) {
ex.printStackTrace();
}
}
}
class Dad {
public void feedBaby() {
System.out.println("Feed the baby...");
}
}
其实,到这种实现方法,我们已经引入了观察者模式。观察者模式的核心就是让行为的行使着变主动为被动。行为的行使着就是观察者。但是从面向对象的角度来讲,有些地方我们还可以接着完善。比如说,小孩什么时候醒的,这个属性不是小孩的,也不是老爸的,而是发生的这件事情本身的,所以醒来这件事应该也是一个类。所以接下来,我们完善一下代码,用第三种方式实现。
package com.robin.test;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
Dad d = new Dad();
Baby b = new Baby(d);
new Thread(b).start();
System.in.read();
}
}
class Baby implements Runnable {
private boolean wakeUp = false;
private Dad dad;
public boolean isWakeUp() {
return wakeUp;
}
void wakeUp() {
wakeUp = true;
}
public Baby(Dad dad) {
this.dad = dad;
}
@Override
public void run() {
try {
Thread.sleep(1000 * 10);
this.wakeUp();
WakeUpEvent wakeUpEvent = new WakeUpEvent(System.currentTimeMillis(), "Home");
dad.ActionToWakeUp(wakeUpEvent);
}
catch (Exception ex) {
ex.printStackTrace();
}
}
}
class Dad {
public void ActionToWakeUp(WakeUpEvent event) {
System.out.println("Feed the baby..." + event.getHappenTime() + event.getLocation());
}
}
class WakeUpEvent {
private long happenTime;
public long getHappenTime() {
return happenTime;
}
public String getLocation() {
return location;
}
private String location;
public WakeUpEvent(long happenTime, String location) {
this.happenTime = happenTime;
this.location = location;
}
}
这里如果我们要求小孩的老妈也要对小孩的醒来进行一个些反应,我们就要修改小孩内部的代码,让小孩同时也持有老妈的引用。面向对象的一个重要原则就是添加而不是修改,就是说可以向小孩添加监听者,而不应该修改小孩的内部代码。所以,现在我们可以让所有的监听者实现同一个接口。而小孩内部持有这个接口的集合,那么就可以添加无数的监听者了。
package com.robin.test;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) throws IOException {
Dad d = new Dad();
Mom m = new Mom();
Baby b = new Baby();
b.getList().add(m);
b.getList().add(d);
new Thread(b).start();
System.in.read();
}
}
class Baby implements Runnable {
private boolean wakeUp = false;
private List list;
public List getList() {
return list;
}
public boolean isWakeUp() {
return wakeUp;
}
void wakeUp() {
wakeUp = true;
}
public Baby() {
this.list = new ArrayList();
}
@Override
public void run() {
try {
Thread.sleep(1000 * 10);
this.wakeUp();
WakeUpEvent wakeUpEvent = new WakeUpEvent(System.currentTimeMillis(), "Home");
for(int i=0; i
所以,最后这版的代码既实现了观察者模式,又有良好的可扩展性。综合来讲,使用观察者模式最大的好处就是减少死循环式的轮循带来的资源无端消耗,并且有着良好的可扩展性。
最后,说一点Java自身一个典型的观察者模式的实现,那就是AWT的Button等GUI的事件监听机制。对于鼠标点击或者键盘输入,Windows操作系统采用的是一种事件派发的机制,操作系统有一个GUI的监听进程,这个进程会不断的监听硬件给的反馈,比如说鼠标点击了一下,或者键盘按了那个字母键。这种行为被操作系统的这个进程捕捉到了以后,它检查到这个事件是哪个具体的程序的,比如说是个AWT程序的,操作系统会告诉虚拟机,鼠标按下了,你进行处理吧。虚拟机有个专门的线程来进行事件处理,这个线程拥有所有的AWT的GUI对象,比如说Button等,都拥有其引用。这个线程接到操作系统的信号后,会把这些信息封装成为一个ActionEvent类,然后检查一下点击了哪个组件,发现是button,就会调用button注册的那些方法。而button注册的那些方法,都是实现了同一个接口,所以button中有一个list,可以注册无数多的接口,而这些接口的具体内容可以由我们自己实现。这种设计有非常好的可扩展性。
综合上边,就是button作为一个观察者,没有时时刻刻监听着鼠标与键盘事件,也没有时时刻刻死循环的去询问操作系统有没有针对自己的点击,而是把自己的引用交给了虚拟机专门的GUI线程,而自己该干嘛干嘛,没有阻塞到这里。