1 定义:
观察者模式(Observer Pattern),也叫做发布订阅模式(Publish / subscribe)
Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.(定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新)
AWT中的事件处理DEM(委派事件模型Delegation Event Model)就是使用观察者模式实现的。
1.1通用类图:
在最基础的观察者模式中,包括以下四个角色:
- 被观察者<目标>(Subject):从类图中可以看到,类中有一个用来存放观察者对象的Vector容器(之所以使用Vector而不使用List,是因为多线程操作时,Vector在是安全的,而List则是不安全的),这个Vector容器是被观察者类的核心,另外还有三个方法:attach方法是向这个容器中添加观察者对象;detach方法是从容器中移除观察者对象;notify方法是依次调用观察者对象的对应方法。这个角色可以是接口,也可以是抽象类或者具体的类,因为很多情况下会与其他的模式混用,所以使用抽象类的情况比较多。
- 具体的被观察观察者<具体目标>(ConcreteSubject):使用这个角色是为了便于扩展,可以在此角色中定义具体的业务逻辑。
- 观察者(Observer):观察者角色一般是一个接口,它只有一个update方法,在被观察者状态发生变化时,这个方法就会被触发调用。
- 具体的观察者(ConcreteObserver):观察者接口的具体实现,在这个角色中,将定义被观察者对象状态发生变化时所要处理的逻辑。
1.2 通用代码:
- public abstract class Subject {
- // 定一个一个观察者数组
- private Vector<Observer> obsVector = new Vector<Observer>();
- // 增加一个观察者
- public void addObserver(Observer o) {
- this.obsVector.add(o);
- }
- // 删除一个观察者
- public void delObserver(Observer o) {
- this.obsVector.remove(o);
- }
- // 通知所有观察者
- public void notifyObserver() {
- for (Observer o : this.obsVector) {
- o.update();
- }
- }
- }
- public interface Observer {
- // 更新方法
- public void update();
- }
- public class ConcreteSubject extends Subject {
- // 具体的业务
- public void doSomething() {
- /*
- * do something
- */
- super.notifyObserver();
- }
- }
- public class ConcreteObserver implements Observer {
- // 实现更新方法
- public void update() {
- System.out.println("接收到信息,并进行处理!");
- }
- }
- public class Test {
- public static void main(String[] args) {
- // 创建一个被观察者
- ConcreteSubject subject = new ConcreteSubject();
- // 定义一个观察则
- Observer obs = new ConcreteObserver();
- // 观察者观察被被观察则
- subject.addObserver(obs);
- // 观察者开始活动了
- subject.doSomething();
- }
- }
2 优点
观察者模式的主要优点如下:
(1) 观察者模式可以实现表示层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层充当具体观察者角色。
(2) 观察者模式在观察目标和观察者之间建立一个抽象的耦合。观察目标只需要维持一个抽象观察者的集合,无须了解其具体观察者。由于观察目标和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次。
(3) 观察者模式支持广播通信,观察目标会向所有已注册的观察者对象发送通知,简化了一对多系统设计的难度。
(4) 观察者模式满足“开闭原则”的要求,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便。
3 缺点
观察者模式的主要缺点如下:
(1) 如果一个观察目标对象有很多直接和间接观察者,将所有的观察者都通知到会花费很多时间。
(2) 如果在观察者和观察目标之间存在循环依赖,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
(3) 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
4 应用场景
4.1 使用关联行为的场景。注意是可拆分的关联,而非组合。
4.2 事件多级触发场景。
4.3 跨系统的消息交换场景,如消息队列的处理机制。(如kkPlayer的全局快捷键)
5 注意事项
5.1 广播链的问题:在一个观察者模式中最多出现一个对象既是观察者又是被观察者,消息转发最多一两次,还比较好控制。
5.2 异步处理问题:需要考虑线程安全和队列的问题。(可以参考Message Queue)
6 扩展
6.1 Java世界中观察者模式:
在前面的通用类图及其源代码实现中,通过观察你也许会发现,抽象的被观察者Subject仅帮我们关联了观察者和确定了通知机制;抽象的观察者Observer仅确立了被通知的方法。 这些完全可以单独抽象出一个联络类,作为观察者或被观察者的职责,这样就很非常符合单一职责原则。 幸运的是,Java从天始诞生就提供了一个可扩展的父类,即Java.util.Observable,这个类专用于让别人去触发,Java.util.Observer接口则专注于对观察者通知
观察者模式在Java语言中的地位非常重要。在JDK的java.util包中,提供了Observable类以及Observer接口,它们构成了JDK对观察者模式的支持。如图22-5所示:
图22-5 JDK提供的Observable类及Observer接口结构图
(1) Observer接口
在java.util.Observer接口中只声明一个方法,它充当抽象观察者,其方法声明代码如下所示:
void update(Observable o, Object arg); |
当观察目标的状态发生变化时,该方法将会被调用,在Observer的子类中将实现update()方法,即具体观察者可以根据需要具有不同的更新行为。当调用观察目标类Observable的notifyObservers()方法时,将执行观察者类中的update()方法。
(2) Observable类
java.util.Observable类充当观察目标类,在Observable中定义了一个向量Vector来存储观察者对象,它所包含的方法及说明见表22-1:
表22-1 Observable类所包含方法及说明
方法名 | 方法描述 |
Observable() | 构造方法,实例化Vector向量。 |
addObserver(Observer o) | 用于注册新的观察者对象到向量中。 |
deleteObserver (Observer o) | 用于删除向量中的某一个观察者对象。 |
notifyObservers()和notifyObservers(Object arg) | 通知方法,用于在方法内部循环调用向量中每一个观察者的update()方法。 |
deleteObservers() | 用于清空向量,即删除向量中所有观察者对象。 |
setChanged() | 该方法被调用后会设置一个boolean类型的内部标记变量changed的值为true,表示观察目标对象的状态发生了变化。 |
clearChanged() | 用于将changed变量的值设为false,表示对象状态不再发生改变或者已经通知了所有的观察者对象,调用了它们的update()方法。 |
hasChanged() | 用于测试对象状态是否改变。 |
countObservers() | 用于返回向量中观察者的数量。 |
我们可以直接使用Observer接口和Observable类来作为观察者模式的抽象层,再自定义具体观察者类和具体观察目标类,通过使用JDK中的Observer接口和Observable类,可以更加方便地在Java语言中应用观察者模式。
6.2观察者模式与Java事件处理:
JDK 1.0及更早版本的事件模型基于职责链模式,但是这种模型不适用于复杂的系统,因此在JDK 1.1及以后的各个版本中,事件处理模型采用基于观察者模式的委派事件模型(DelegationEvent Model, DEM),即一个Java组件所引发的事件并不由引发事件的对象自己来负责处理,而是委派给独立的事件处理对象负责。
在DEM模型中,目标角色(如界面组件)负责发布事件,而观察者角色(事件处理者)可以向目标订阅它所感兴趣的事件。当一个具体目标产生一个事件时,它将通知所有订阅者。事件的发布者称为事件源(Event Source),而订阅者称为事件监听器(Event Listener),在这个过程中还可以通过事件对象(Event Object)来传递与事件相关的信息,可以在事件监听者的实现类中实现事件处理,因此事件监听对象又可以称为事件处理对象。事件源对象、事件监听对象(事件处理对象)和事件对象构成了Java事件处理模型的三要素。事件源对象充当观察目标,而事件监听对象充当观察者。以按钮点击事件为例,其事件处理流程如下:
(1) 如果用户在GUI中单击一个按钮,将触发一个事件(如ActionEvent类型的动作事件),JVM将产生一个相应的ActionEvent类型的事件对象,在该事件对象中包含了有关事件和事件源的信息,此时按钮是事件源对象;
(2) 将ActionEvent事件对象传递给事件监听对象(事件处理对象),JDK提供了专门用于处理ActionEvent事件的接口ActionListener,开发人员需提供一个ActionListener的实现类(如MyActionHandler),实现在ActionListener接口中声明的抽象事件处理方法actionPerformed(),对所发生事件做出相应的处理;
(3) 开发人员将ActionListener接口的实现类(如MyActionHandler)对象注册到按钮中,可以通过按钮类的addActionListener()方法来实现注册;
(4) JVM在触发事件时将调用按钮的fireXXX()方法,在该方法内部将调用注册到按钮中的事件处理对象的actionPerformed()方法,实现对事件的处理。
使用类似的方法,我们可自定义GUI组件,如包含两个文本框和两个按钮的登录组件LoginBean,可以采用如图22-6所示设计方案:
图22-6 自定义登录组件结构图【省略按钮、文本框等界面组件】
图22-6中相关类说明如下:
(1) LoginEvent是事件类,它用于封装与事件有关的信息,它不是观察者模式的一部分,但是它可以在目标对象和观察者对象之间传递数据,在AWT事件模型中,所有的自定义事件类都是java.util.EventObject的子类。
(2) LoginEventListener充当抽象观察者,它声明了事件响应方法validateLogin(),用于处理事件,该方法也称为事件处理方法,validateLogin()方法将一个LoginEvent类型的事件对象作为参数,用于传输与事件相关的数据,在其子类中实现该方法,实现具体的事件处理。
(3) LoginBean充当具体目标类,在这里我们没有定义抽象目标类,对观察者模式进行了一定的简化。在LoginBean中定义了抽象观察者LoginEventListener类型的对象lel和事件对象LoginEvent,提供了注册方法addLoginEventListener()用于添加观察者,在Java事件处理中,通常使用的是一对一的观察者模式,而不是一对多的观察者模式,也就是说,一个观察目标中只定义一个观察者对象,而不是提供一个观察者对象的集合。在LoginBean中还定义了通知方法fireLoginEvent(),该方法在Java事件处理模型中称为“点火方法”,在该方法内部实例化了一个事件对象LoginEvent,将用户输入的信息传给观察者对象,并且调用了观察者对象的响应方法validateLogin()。
(4) LoginValidatorA和LoginValidatorB充当具体观察者类,它们实现了在LoginEventListener接口中声明的抽象方法validateLogin(),用于具体实现事件处理,该方法包含一个LoginEvent类型的参数,在LoginValidatorA和LoginValidatorB类中可以针对相同的事件提供不同的实现。
6.3 观察者模式与MVC:
在当前流行的MVC(Model-View-Controller)架构中也应用了观察者模式,MVC是一种架构模式,它包含三个角色:模型(Model),视图(View)和控制器(Controller)。其中模型可对应于观察者模式中的观察目标,而视图对应于观察者,控制器可充当两者之间的中介者。当模型层的数据发生改变时,视图层将自动改变其显示内容。如图22-7所示:
图22-7 MVC结构示意图
在图22-7中,模型层提供的数据是视图层所观察的对象,在视图层中包含两个用于显示数据的图表对象,一个是柱状图,一个是饼状图,相同的数据拥有不同的图表显示方式,如果模型层的数据发生改变,两个图表对象将随之发生变化,这意味着图表对象依赖模型层提供的数据对象,因此数据对象的任何状态改变都应立即通知它们。同时,这两个图表之间相互独立,不存在任何联系,而且图表对象的个数没有任何限制,用户可以根据需要再增加新的图表对象,如折线图。在增加新的图表对象时,无须修改原有类库,满足“开闭原则”。
6.4 项目中真实的观察者模式:
因为前面讲解的都是太标准的模式,在系统设计中会对观察者模式进行改造或改装,主要是下面三方面。
A。观察者与被观察者之间的消息沟通:被观察者状态改变时会触发观察者的一个行为,同时会传递一个消息给观察者,在实际中一般的做法是:观察者中的update方法接受两个参数,一个是被观察者,一个是DTO(Data Transfer Object,数据传输对象),DTO一般是一个纯JavaBean,由被观察者生成,由观察者消费。(若远程,则以XML格式传递)
B。观察者响应方式:观察者是个比较复杂的逻辑,要接受被观察者传递过来的信息,同时还要对他们进行逻辑处理,如果一个观察者对应多个被观察者,则需要考虑性能。备选方法两个:一是多线程,一是缓存技术。
C。被观察者尽量自己做主:不要把消息传到观察者时才判断是否需要消费。
应用举例:
文件系统:当在目录下新建一个文件,这个动作会同时通知目录管理器增加该目录,并通知磁盘管理器减少1KB的空间,也就是“文件”是一个被观察者,“目录管理器”则是观察者。
猫鼠游戏:猫叫一声,惊动了鼠;
广播收音机:电台在广播,收音机就能收听。
7 范例
7.1 这里使用原书例子,使用java提供的观察者服务类。
类图如下:
需要注意的是,源码中有模拟处理耗时的情况,当然默认并没有多线程支持。。。
源代码如下:(作者原书例)
- import java.util.Observable;
- /**
- * @author cbf4Life cbf4life@126.com I'm glad to share my knowledge with you
- * all. 韩非子,李斯的师弟,韩国的重要人物
- */
- public class HanFeiZi extends Observable {
- // 韩非子要吃饭了
- public void haveBreakfast() {
- System.out.println("韩非子:开始吃饭了...");
- // 通知所有的观察者
- super.setChanged();
- super.notifyObservers("韩非子在吃饭");
- }
- // 韩非子开始娱乐了,古代人没啥娱乐,你能想到的就那么多
- public void haveFun() {
- System.out.println("韩非子:开始娱乐了...");
- super.setChanged();
- this.notifyObservers("韩非子在娱乐");
- }
- }
- public class LiSi implements Observer {
- // 首先李斯是个观察者,一旦韩非子有活动,他就知道,他就要向老板汇报
- public void update(Observable observable, Object obj) {
- System.out.println("李斯:观察到李斯活动,开始向老板汇报了...");
- this.reportToQiShiHuang(obj.toString());
- System.out.println("李斯:汇报完毕,秦老板赏给他两个萝卜吃吃...\n");
- try {
- System.out.println("我开始休眠 " + System.currentTimeMillis());
- Thread.sleep(3000);
- System.out.println("我起来了 " + System.currentTimeMillis());
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- // 汇报给秦始皇
- private void reportToQiShiHuang(String reportContext) {
- System.out.println("李斯:报告,秦老板!韩非子有活动了--->" + reportContext);
- }
- }
- public class LiuSi implements Observer {
- // 刘斯,观察到韩非子活动后,自己也做一定得事情
- 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 + ",——所以我快乐呀!");
- }
- }
- public class WangSi implements Observer {
- // 王斯,看到李斯有活动,自己就受不了
- public void update(Observable observable, Object obj) {
- System.out.println("Observable = " + observable.getClass()
- + " , obj = " + obj);
- System.out.println("王斯:观察到韩非子活动,自己也开始活动了...");
- this.cry(obj.toString());
- System.out.println("王斯:真真的哭死了...\n");
- }
- // 一看李斯有活动,就哭,痛哭
- private void cry(String context) {
- System.out.println("王斯:因为" + context + ",——所以我悲伤呀!");
- }
- }
- 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(wangSi);
- hanFeiZi.addObserver(liuSi);
- hanFeiZi.addObserver(liSi);
- // 然后这里我们看看韩非子在干什么
- hanFeiZi.haveBreakfast();
- }
- }
测试结果:
韩非子:开始吃饭了...
李斯:观察到李斯活动,开始向老板汇报了...
李斯:报告,秦老板!韩非子有活动了--->韩非子在吃饭
李斯:汇报完毕,秦老板赏给他两个萝卜吃吃...
我开始休眠 1337226592828
我起来了 1337226595828
刘斯:观察到韩非子活动,开始动作了...
刘斯:因为韩非子在吃饭,——所以我快乐呀!
刘斯:真被乐死了
Observable = class _16_Observer.HanFeiZi , obj = 韩非子在吃饭
王斯:观察到韩非子活动,自己也开始活动了...
王斯:因为韩非子在吃饭,——所以我悲伤呀!
王斯:真真的哭死了...
7.2 多人联机对战游戏的设计
Sunny软件公司欲开发一款多人联机对战游戏(类似魔兽世界、星际争霸等游戏),在该游戏中,多个玩家可以加入同一战队组成联盟,当战队中某一成员受到敌人攻击时将给所有其他盟友发送通知,盟友收到通知后将作出响应。 Sunny软件公司开发人员需要提供一个设计方案来实现战队成员之间的联动。 |
Sunny软件公司开发人员通过对系统功能需求进行分析,发现在该系统中战队成员之间的联动过程可以简单描述如下:
联盟成员受到攻击-->发送通知给盟友-->盟友作出响应。
如果按照上述思路来设计系统,由于联盟成员在受到攻击时需要通知他的每一个盟友,因此每个联盟成员都需要持有其他所有盟友的信息,这将导致系统开销较大,因此Sunny公司开发人员决定引入一个新的角色——“战队控制中心”——来负责维护和管理每个战队所有成员的信息。当一个联盟成员受到攻击时,将向相应的战队控制中心发送求助信息,战队控制中心再逐一通知每个盟友,盟友再作出响应,如图22-2所示:
图22-2 多人联机对战游戏中对象的联动
在图22-2中,受攻击的联盟成员将与战队控制中心产生联动,战队控制中心还将与其他盟友产生联动。
如何实现对象之间的联动?如何让一个对象的状态或行为改变时,依赖于它的对象能够得到通知并进行相应的处理?
别着急,本章所介绍的观察者模式将为对象之间的联动提供一个优秀的解决方案,下面就让我们正式进入观察者模式的学习。
为了实现对象之间的联动,Sunny软件公司开发人员决定使用观察者模式来进行多人联机对战游戏的设计,其基本结构如图22-4所示:
图22-4 多人联机对战游戏结构图
在图22-4中,AllyControlCenter充当目标类,ConcreteAllyControlCenter充当具体目标类,Observer充当抽象观察者,Player充当具体观察者。完整代码如下所示:
- import java.util.*;
- //抽象观察类
- interface Observer {
- public String getName();
- public void setName(String name);
- public void help(); //声明支援盟友方法
- public void beAttacked(AllyControlCenter acc); //声明遭受攻击方法
- }
- //战队成员类:具体观察者类
- class Player implements Observer {
- private String name;
- public Player(String name) {
- this.name = name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public String getName() {
- return this.name;
- }
- //支援盟友方法的实现
- public void help() {
- System.out.println("坚持住," + this.name + "来救你!");
- }
- //遭受攻击方法的实现,当遭受攻击时将调用战队控制中心类的通知方法notifyObserver()来通知盟友
- public void beAttacked(AllyControlCenter acc) {
- System.out.println(this.name + "被攻击!");
- acc.notifyObserver(name);
- }
- }
- //战队控制中心类:目标类
- abstract class AllyControlCenter {
- protected String allyName; //战队名称
- protected ArrayList<Observer> players = new ArrayList<Observer>(); //定义一个集合用于存储战队成员
- public void setAllyName(String allyName) {
- this.allyName = allyName;
- }
- public String getAllyName() {
- return this.allyName;
- }
- //注册方法
- public void join(Observer obs) {
- System.out.println(obs.getName() + "加入" + this.allyName + "战队!");
- players.add(obs);
- }
- //注销方法
- public void quit(Observer obs) {
- System.out.println(obs.getName() + "退出" + this.allyName + "战队!");
- players.remove(obs);
- }
- //声明抽象通知方法
- public abstract void notifyObserver(String name);
- }
- //具体战队控制中心类:具体目标类
- class ConcreteAllyControlCenter extends AllyControlCenter {
- public ConcreteAllyControlCenter(String allyName) {
- System.out.println(allyName + "战队组建成功!");
- System.out.println("----------------------------");
- this.allyName = allyName;
- }
- //实现通知方法
- public void notifyObserver(String name) {
- System.out.println(this.allyName + "战队紧急通知,盟友" + name + "遭受敌人攻击!");
- //遍历观察者集合,调用每一个盟友(自己除外)的支援方法
- for(Object obs : players) {
- if (!((Observer)obs).getName().equalsIgnoreCase(name)) {
- ((Observer)obs).help();
- }
- }
- }
- }
编写如下客户端测试代码:
- class Client {
- public static void main(String args[]) {
- //定义观察目标对象
- AllyControlCenter acc;
- acc = new ConcreteAllyControlCenter("金庸群侠");
- //定义四个观察者对象
- Observer player1,player2,player3,player4;
- player1 = new Player("杨过");
- acc.join(player1);
- player2 = new Player("令狐冲");
- acc.join(player2);
- player3 = new Player("张无忌");
- acc.join(player3);
- player4 = new Player("段誉");
- acc.join(player4);
- //某成员遭受攻击
- Player1.beAttacked(acc);
- }
- }
编译并运行程序,输出结果如下:
金庸群侠战队组建成功! ---------------------------- 杨过加入金庸群侠战队! 令狐冲加入金庸群侠战队! 张无忌加入金庸群侠战队! 段誉加入金庸群侠战队! 杨过被攻击! 金庸群侠战队紧急通知,盟友杨过遭受敌人攻击! 坚持住,令狐冲来救你! 坚持住,张无忌来救你! 坚持住,段誉来救你! |
在本实例中,实现了两次对象之间的联动,当一个游戏玩家Player对象的beAttacked()方法被调用时,将调用AllyControlCenter的notifyObserver()方法来进行处理,而在notifyObserver()方法中又将调用其他Player对象的help()方法。Player的beAttacked()方法、AllyControlCenter的notifyObserver()方法以及Player的help()方法构成了一个联动触发链,执行顺序如下所示:
Player.beAttacked() --> AllyControlCenter.notifyObserver() -->Player.help()。