写点什么

从观察者模式到 Java 事件处理机制(上)

作者:老农小江
  • 2022-12-07
    广东
  • 本文字数:7008 字

    阅读完需:约 23 分钟

一、前言

事件事件监听,这样的词在我们日常工作中并不少见,尤其是在一些架构设计、中间件源码中,相关的概念、代码更是屡见不鲜,可见其在编程世界里扮演着举足轻重的角色。然而,有些同学在实际应用事件相关的理念时,仍然可能有些困难,本文将逐步地说明事件的相关概念与应用实践,以便于深入理解事件。

事件模型,通俗来说是一种设计思想,是对实际应用场景的一种抽象模型设计(有的地方,也叫事件驱动模型、事件模式、事件机制等,通常指的是同一个意思)。在细说事件模型之前,我们先说一下观察者模式,Java 的事件处理机制,在一定程度上来说,就是是观察者模式的一种变种或者说升级实现,而观察者模式相对更好理解一些,在理解了观察者模式之后,再理解事件模型就水到渠成了。


二、观察者模式

2.1、基本概念

​ 观察者模式也是一种编程设计思想,是经典的 23 种设计模式之一,其大致定义是:当对象之间存在一种一对多的依赖关系时,每当被依赖对象的状态(行为)发生变化时,所有依赖于它的对象都会得到通知并自动更新(进行自己相应的行为操作)。其主要应对的场景是,一个对象的状态改变给其它依赖对象通知的问题,并且要相对优雅地处理这个问题(代码低耦合、易扩展等)。

​ 生活中,适用于观察者模式的场景很多,比方说:过马路的人们观察到红绿灯变绿了,人们开始过马路;跑步运动员观察到裁判员起跑发令枪响了,开始跑步;球迷观察到球员进球了,开始欢呼。类似这种一对多的依赖关系,实际生活场景之中比比皆是。

​ 大家注意到没有,以上的例子中,对象关系之间有两个主要的角色,分别是被观察者观察者,而被观察者虽然有个"被"字,但其实是掌握着整个事情行为的主动权的。比方说,是红绿灯主动变绿了,人们才能观察到,然后过马路的;是裁判主动打响了发令枪,运动员才听到枪响开始跑步的,理解这个主动/被动关系非常重要。

​ 我们用裁判员、运动员的例子来分析一下整个过程当中的的各角色行为。作为观察者的运动员,只要做一件事情,那就是观察到裁判员的发令枪响后开始跑(也就是听到发令枪响,收到起跑通知);而作为被观察者的裁判员则需做两件事情,一是打响发令枪,二是打响发令枪后,通知所有参赛运动员开始跑(发令枪响了,就是通知)。当然,作为裁判员,他需要知道到底要通知哪些参赛运动员,所以在裁判员对象中,应当包含需要通知的运动员列表,并且可以新增需要通知的运动员、删除不再需要通知的运动员。

2.2、应用示例

​ 程序是生活的抽象实现,通过上面的初步分析,我们可以对裁判员、运动员的例子进行相关的接口/类设计如下图所示:

简单说明一下设计图中的接口/类:

AbsObservable

​ 被观察者抽象类;维护一个观察者列表,抽象定义增加观察者、删除观察者、通知观察者的公共方法。

IObserver

​ 观察者接口;抽象定义观察者被通知后的行为方法。

Referee

​ 裁判员对象;具体的被观察者,继承被观察者抽象类,同时定义了自己的业务逻辑方法。

Sportsman

​ 运动员对象;具体的观察者,实现观察者接口,并在实现方法中完成自己特定的业务逻辑。

接下来,我们看一下这个场景的具体代码实现。

1、被观察者抽象类AbsObservable

import java.util.Iterator;import java.util.Objects;import java.util.Vector;
/** * 被观察者 抽象类 * * @author : laonong */public abstract class AbsObservable {
/** * 观察者 列表 */ private Vector<IObserver> observers;
public AbsObservable() { this.observers = new Vector<>(); }
/** * 增加 观察者 * @param observer */ public synchronized void addObserver(IObserver observer) { if(Objects.isNull(observer)){ throw new NullPointerException(); } if(!this.observers.contains(observer)){ this.observers.addElement(observer); } }
/** * 删除 观察者 * @param observer */ public synchronized void delObserver(IObserver observer) { this.observers.removeElement(observer); }
/** * 通知 所有观察者 * @param args 通知观察者时,传递的参数 */ public synchronized void notifyObservers(Object args){ Iterator<IObserver> iterator = this.observers.iterator(); while (iterator.hasNext()) { iterator.next().update(this,args); } }}
复制代码

2、观察者接口IObserver

/** * 观察者 抽象接口定义 * * @author : laonong */public interface IObserver {
/** * 当被观察者状态变化时,会回调这里 通知观察者 * @param observable 被观察者对象 * @param args 通知的参数对象 */ void update(AbsObservable observable,Object args);}
复制代码

3、具体被观察者 裁判员对象Referee

/** * 裁判员(被观察者) * * @author : laonong */public class Referee extends AbsObservable {
/** * 裁判员名字 */ private String name;
public String getName() { return name; }
public Referee(String name) { this.name = name; }
/** * 发出起跑命令 * @param args */ public void startCommand(Object args){ System.out.println("裁判员" + this.name+ "说:" + args);
//通知所有的观察者 super.notifyObservers(args); }}
复制代码

4、具体观察者 运动员对象 Sportsman

/** * 运动员(观察者) * * @author : laonong */public class Sportsman implements IObserver {
/** * 运动员名字 */ private String name;
public Sportsman(String name) { this.name = name; }
/** * 收到通知,开始跑 * @param observable 被观察者对象 * @param args 通知的参数 */ @Override public void update(AbsObservable observable, Object args) { Referee referee = (Referee) observable; System.out.println("运动员 " + this.name + " 观察到(听到)裁判员" + referee.getName() + "的命令:" + args + "。" + this.name + "开始跑。"); }}
复制代码

接下来,我们模拟一下裁判员打响发令枪,运动员观察到后开始跑的调用过程:

/** * 观察者模式 执行模拟 * * @author : laonong */public class Client {    public static void main(String[] args) {        //1、裁判员 (被观察者)        Referee referee = new Referee("小明");
//2、运动员 (观察者) Sportsman daqiang = new Sportsman("大强"); Sportsman dazhuang = new Sportsman("大壮");
//3、将运动员(观察者)添加到裁判员(被观察者)列表中 referee.addObserver(daqiang); referee.addObserver(dazhuang);
//4、裁判员发出起跑命令 referee.startCommand("大家开始跑啊"); }}
复制代码

执行后输出:

裁判员小明说:大家开始跑啊运动员 大强 观察到(听到)裁判员小明的命令:大家开始跑啊。大强开始跑。运动员 大壮 观察到(听到)裁判员小明的命令:大家开始跑啊。大壮开始跑。
复制代码

以上就是观察者模式应用的一个基础版的演示,大家可以回过头来看一看、想一想,是不是还是比较好理解的。当然,实际工作中的应用可能会比这个示例更复杂一些,但核心设计思想基本是一样的。

​ 贴心的JDK提供了观察者、被观察者两个角色的抽象接口/实现类(本文基于 JDK1.8),分别是java.util.Observer接口、java.util.Observable类。大家可以看一下这两个类的源代码,会发现和笔者自定义的非常相似,在实际应用中,通常情况下,就不用自己抽象定义了,当需要时,具体观察者直接实现java.util.Observer接口,具体被观察者直接继承java.util.Observable类就可以了。(本文的演示代码都是基于 Java 的,但实际上不论什么开发语言,思想是通用的,所以,理解了设计思想对于我们来说基本就一通百通了。)

2.3、优点缺点

​ 观察者模式作为一种设计思想,有其优秀的地方,自然也会有所不足,在实际应用场景中,通常会比笔者演示的示例要更复杂一些,需要考虑的问题也就会更多一些。

​ 此处对观察者主要的优缺点进行简单说明:

优点

​ 1、观察者、被观察者之间是上层抽象耦合的,有利于代码的扩展;(比方说:如果需要一个监督员来监督(观察)裁判员发出起跑命令的行为是否合规,只要定义一个监督员对象实现观察者接口,然后在实现方法中写自己的业务逻辑就可以了,非常地方便。)

​ 2、构建了一套通知机制,有利于提高代码逻辑的清晰度;(比方说:运动员观察到裁判员发令枪响后开始跑,现场观众观察到运动员开始跑后,就开始呐喊加油,摄影师观察到观众呐喊后,开始抓拍照片。这是一系列的多级依赖关系,如果不使用观察者模式的话,程序代码要表达这一行为过程会很繁杂,而使用观察者模式,程序代码就可以更清晰地描述这个过程。)

​ 3、提升了程序代码的规范性、可重用性。(对象的公共行为、依赖关系都抽象到了上层接口/抽象类中)

缺点

​ 1、可能会引发性能问题。因为被观察者通知所有观察者时,是逐个按顺序同步通知的(由 JDK 源码可知,是 for 循环调用通知),所以如果观察者较多的话,通知完成可能时间较长,从而被观察者需要等待较久,可能出现“卡顿”的现象,如果是多级观察(依赖)的话(或者不小心引发循环依赖),这种风险就更大了,甚至可能直接就“卡死”了,所以在实际应用时,需要仔细考虑。当然,可以做一些变种优化,比方说"通知"改成异步的,或者控制/减少观察者的数量、层级,优化观察者自有业务的执行效率等。

​ 2、可能会带来开发/调式的的困难。因为观察者模式可以有众多观察者,且可以多级依赖“观察”,从而形成一个类似“链式”调用,如果在设计/开发时,没有控制好的话,反而可能带来开发/调式的繁琐,稍微想想那种层层往返调试的场景都头大。所以,在使用观察者模式时,一定要设计规划好,否则,反而会带来反效果。(类似责任链模式、装饰模式基本都有类似的问题,实际应用时都不应该有太多层/依赖,否则后续维护会非常难受。)


tips:

​ 观察者模式多级依赖下的“链式”调用,和责任链模式下的链式调用还是有所区别的,其中重要的区别在于:观察者模式的通知消息在传播链中,消息内容一般是变化的,是由当前节点对象、相邻下级节点对象“协商”决定通知什么消息内容给下一节点的(比方说,裁判员、运动员协商通知什么消息给运动员,运动员又该如何处理裁判员的通知消息;运动员、粉丝协商决定通知什么消息给粉丝,粉丝又该如何处理运动员的通知消息;裁判员、粉丝之间是没有直接关系的)。而在责任链模式下,消息内容自第一个节点开始逐级传播后,消息内容一般是不变的,链条中的每一个节点要处理的消息内容都是相同的(可能会在原始消息内容上略加补充),都是对同样的消息内容校验是否需要自己处理(如果能到当前节点的话)。

2.4、适用场景

​ 由上文可知,观察者模式作为一种设计思想(具体实现可能略有差异,JDK 提供了一种实现),有其非常优秀的地方,也有所不足,通常来说,如果实际业务场景有以下特点的话,可以考虑使用。

​ 1、对象/行为之间存在某种一对多的关联依赖关系,当一个对象发生变化时,需通知其它依赖对象,但并不需要其它对象的执行细节;

​ 2、对象之间存在某种触发依赖链的关系,比方说,对象 A 会触发对象 B 的某种行为,对象 B 又会触发对象 C 的某种行为;

​ 3、业务系统需要跨模块或者跨系统通知调用,希望使用消息通知机制,但是又不想引入较重的消息中间件,且对消息通知的可靠性要求不是特别高的情况下,可以考虑使用观察者模式这种相对轻量级的通知机制。(引入消息中间件,当然可以更好的应对消息通知的场景,但同时这也意味着更多的组件依赖,更多的资源消耗,更复杂的调用链路。)


tips:

​ 观察者模式,有的地方也叫发布订阅模式,不过这两者之间还是有一些区别的,发布订阅模式应该说是观察者模式的一种变种。相对于观察者模式主要有观察者、被观察者这两个角色,发布订阅模式通常还有一个经纪人(Broker)的中间角色,通过这一角色的引入,优化了观察者模式的不足(发布者、订阅者之间不存在耦合),进一步降低了观察者(订阅者)、被观察者(发布者)之间的依赖关系,提高了整体的处理能力与可靠性。

​ 实际业务需求场景中,在需要用到消息通知机制的时候,我们更多地会选择引入独立的消息中间件来解决问题,因为这样相对更简单、高效、可靠。但是,如果是我们自己需要做一个相对独立的中间件系统(或者技术框架)的话,更好的选择则是根据观察者模式/发布订阅模式的设计思想,自己设计实现相关需要的功能,因为这种情况下依赖独立的消息中间件的话,就显得太“笨重”了。

2.5、示例扩展

​ 思考一下,对于裁判员、运动员的场景,如果还有一个新的对象粉丝,在运动员开始跑后,粉丝就开始呐喊加油,这时我们该如何处理呢?我们在以上示例的基础上稍加修改,就可以实现这个场景下的代码逻辑。

​ 分析一下,这种情况下,我们一共有裁判员、运动员、粉丝这 3 个对象了,此时对象之间的关系如下图所示:

首先,裁判员对象作为被观察者,没有新的关系/行为产生,所以不需要做任何改变。

其次,我们需要新增一个粉丝对象,作为运动员对象的观察者。

Fans对象代码如下:

/** * 粉丝(观察者) * * @author : laonong */public class Fans implements IObserver {    /**     * 粉丝名字     */    private String name;
public Fans(String name) { this.name = name; }
@Override public void update(AbsObservable observable, Object args) { Sportsman sportsman = (Sportsman) observable; System.out.println("粉丝 " + this.name + " 观察到(看到)运动员" + sportsman.getName() + args + "。" + this.name + "开始欢呼加油。"); }}
复制代码

再有,此时的运动员对象除了作为裁判员对象的观察者外,还成为了粉丝对象的被观察者,所以也需要实现AbsObservable抽象类,此外,作为被观察者,在执行完成自己特定的业务逻辑后,需要“通知”它的所有观察者。

Sportsman对象微调后的代码如下:

/** * 运动员 (观察者、被观察者) * * @author : laonong */public class Sportsman extends AbsObservable implements IObserver {
/** * 运动员名字 */ private String name;
public String getName() { return name; }
public Sportsman(String name) { this.name = name; }
/** * 收到通知,开始跑 * @param observable * @param args */ @Override public void update(AbsObservable observable, Object args) { Referee referee = (Referee) observable; System.out.println("运动员 " + this.name + " 观察到(听到)裁判员" + referee.getName() + "的命令:" + args + "。" + this.name + "开始跑。");
/** * 运动员对象,此时作为粉丝对象的偶像(被观察者), * 在自己开始跑后(完成自己的业务逻辑),需要告诉一下喜欢他的所有粉丝(通知其所有观察者)。 */ super.notifyObservers("开始跑"); }}
复制代码

至此,我们就完成了新的场景下的代码扩展,是不是很简单!简单就对了,这正是观察者模式的魅力所在,下面我们模拟一下新的场景调用过程:

/** * 观察者模式 执行模拟 * * @author : laonong */public class Client {    public static void main(String[] args) {        //1、裁判员 (被观察者)        Referee referee = new Referee("小明");
//2、运动员 (观察者) Sportsman daqiang = new Sportsman("大强"); Sportsman dazhuang = new Sportsman("大壮");
//3、将运动员(观察者)添加到裁判员(被观察者)列表中 referee.addObserver(daqiang); referee.addObserver(dazhuang); /** * 运动员大强 ,有两个铁杆粉丝铁蛋、铁头,一直在观察大强的起跑。 */ daqiang.addObserver(new Fans("铁蛋")); daqiang.addObserver(new Fans("铁头"));
//4、裁判员发出起跑命令 referee.startCommand("大家开始跑啊"); }}
复制代码

执行后输出:

裁判员小明说:大家开始跑啊运动员 大强 观察到(听到)裁判员小明的命令:大家开始跑啊。大强开始跑。粉丝 铁蛋 观察到(看到)运动员大强开始跑。铁蛋开始欢呼加油。粉丝 铁头 观察到(看到)运动员大强开始跑。铁头开始欢呼加油。运动员 大壮 观察到(听到)裁判员小明的命令:大家开始跑啊。大壮开始跑。
复制代码

注意看输出结果的输出顺序(执行顺序)!思考一下示例场景的扩展实现,再回过头来看一下前面所说的优缺点、适用场景,是不是对观察者模式有了更深一点的理解呢?


下篇请看 从观察者模式到Java事件处理机制(下)


越学越无知,我是老农小江,欢迎交流~

发布于: 刚刚阅读数: 5
用户头像

老农小江

关注

好好学习,天天向上 2020-03-26 加入

越学越无知,一个普通老码农,欢迎交流~

评论

发布
暂无评论
从观察者模式到Java事件处理机制(上)_设计模式_老农小江_InfoQ写作社区