使用事件驱动的简单例子(观察者模式)

使用事件驱动的简单例子(观察者模式)

假设你正在开发一个博客系统。现在有这样一个需求:“当作者创建(写完并保存)、修改、删除了一篇文章后,发邮件通知博主,并且对文章进行索引和评分”。你会怎么做呢?

实际上,这就是“达到什么什么条件,就做什么什么”的场景。完全可以用设计模式来解耦。

参考:https://blog.csdn.net/hackxiaof/article/details/51690201

 

一、很捞的写法

(1)简单实现

直接理解需求,现在存在着三个主要功能:创建、修改、删除文章。那么简单愉快地实现吧!

BadExample.java

看上去相当简单直观,没错吧!

(2)辅助功能

接下来把辅助功能写了,并且调用之。

看起来还不错,是吧!接下来看看这样写会有什么问题。

(3)问题来了

现在更多的需求来了,当作者创建一篇文章的同时,不仅需要“发邮件通知博主,并且对文章进行索引和评分”,还需要:

  1. 自动把文章推荐给该作者的所有订阅者。
  2. 把文章加入“最新博文”列表。(列表展示最新博文,有两种解决方案:把最新文章加入列表头部/client每次打开列表的时候做一次排序)
  3. 提升作者活跃度。
  4. 作者达到一定活跃度,把作者推上排行榜。
  5. 统计写作数量,达到一定数量,提升作者等级。
  6. …..

如果继续上面的做法,一个简单的方法就会变成这样:

现在回顾一下这个create方法,是不是感觉变臃肿了?我只想简简单单地创建一篇文章而已,结果在主要功能之后又来了一大堆乱七八糟的东西。现在还只是五个需求而已,想想以后可能还会多出四五十个需求,就令人头皮发麻。

事实上,对于这种“达到什么什么条件,就做什么什么”的场景,一开始就可以用事件驱动的思想进行设计。

二、事件驱动的写法

事件驱动,简单来说就是:我写了一篇文章,就抛出一个事件。当监听这个事件的处理器收到这个事件后,就做一些相关的工作。

(1)概念

1.参考文章中的概念

在参考文章中,作者认为,事件的分发和监听有这样几个概念:

  1. 事件源:触发事件的对象。在上面的场景中,实体对象就是事件源(文章)。事件:对事件源的操作会产生事件。
  2. 发表文章、修改文章、删除文章这些操作就会触发相关的文章被发表事件、文章被删除事件、文章被修改事件。
  3. 事件监听器:对事件源各种事件触发执行行为的抽象,包括接口和若干实现类。比如: 接口需要定义事件源,相关事件触发时需要实现的操作接口。
  4. 事件分发器:事件分发器主要处理事件的分发和事件监听器的管理,具有注册和删除事件监听器等功能。

2.我的理解

上面的概念比较抽象,不太好理解。个人认为,想要实现事件驱动,需要理解以下几个组成部分:

  1. 实体。对实体的操作将会发出事件。在上面的场景中,实体就是“文章”。对实体进行操作的时候,我们会抛出一个相应的事件对象。(举个例子:创建新文章时,抛出一个文章事件。)
  2. 事件。事件也是一个对象,事件对象应该“说明”实体发生了什么操作,并且封装了处理器类需要的所有信息,供处理器类使用。(举个例子:创建新文章时,抛出一个文章事件。事件类型是“新建文章”,封装了文章对象本身、创建日期等等信息。)
  3. 事件监听器。事件监听器储存(管理)了所有的事件处理器,并且负责监听相应事件的发生。一旦监听到了某个事件,监听器就会告知所有的事件处理器,对事件进行处理。(举个例子:事件监听器A管理着B、C、D三个事件处理器。当事件A收到了事件E时,就会告知B、C、D对事件E进行相应的处理。)
  4. 事件处理器。事件处理器负责处理事件。(举个例子:邮件事件处理器收到了“新建文章”这个事件后,就会发进行“发送邮件”的处理。)

3.回顾观察者模式

如果只从字面上去理解,“观察者”一直呆在一边持续观察,就像篮球场上的裁判一样,一直观察着球员的动作;一旦球员犯规了,裁判就会出示一张红牌。在这个过程中,球员只需要专注比赛,裁判会自发地完成所有的观察工作。

但是,如果要用语言实现这个场景(实现“持续”这个效果),就没这么优雅了。根据面向对象的设计原则,裁判的“观察”应该写成一个observe方法,“持续观察”就应该在observe方法里写一个while或者for循环。最后,裁判的表现就变成了:“我观察观察观察观察…”。暂且不论这个场景有多么滑稽,从性能上来说,这种做法就是非常低效的。

如果理解不了上面的场景,不妨把上面的裁判想像成一个非常关心你的老妈。从现在开始,老妈每秒钟都问你一次“你吃了吗?”,如果发现你吃了,她才去收拾碗筷。这样一来,如果你一个小时内没吃饭,老妈就要不停询问3600次…作为一个孝顺儿子,我们当然不能让这种状况发生。

个人认为,观察者模式就是把“主动观察”变成“主动告知”,要求被观察者自觉地通知观察者。拿上面的篮球裁判做个例子,只要球员一犯规就主动通知裁判(发送给裁判犯规事件),裁判就不用一直盯着球员看了(不需要轮询),直接给出一张红牌即可(处理事件)。

再拿上面的老妈作个例子,现在老妈再也不问你“你吃了吗?”(不需要轮序),而是等你自己吃完,告诉她“我吃完了”(发送给老妈吃完事件),老妈才会去收拾碗筷(处理事件)。

(2)实现

项目结构如下:

fff

1.先写一个文章实体

Article.java

既然要写文章,当然要创建一个文章实体。

2.写一个事件实体

既然我们要发出一个事件,就需要一个事件实体。实体中包含了事件的状态:

ArticleEvent.java

事件类中应该包含处理器类需要的所有信息(处理器需要的数据完全是事件类提供的)。

3.写一个事件监听类

接下来写一个事件监听类。监听类中储存了所有处理器类的信息。一旦收到了事件,这个监听类就会根据事件类型,把事件交给对应的处理器类处理。

EventListener.java

只要主动创建事件并且调用postEvent方法,就相当于把事件推送给了监听器,监听器就会去找相应的事件处理器(球员主动把犯规事件告诉裁判,裁判根据犯规事件的属性做相应的处理)。

4.写处理器接口与实现类

处理器类负责处理收到的事件。先写一个接口:

Processor.java

再写出具体的实现类:

EmailProcessor.java

IndexProcessor.java

ScoreProcessor.java

5.测试类

写一个测试类:

Mock.java

首先把处理器类注册到监听类中,然后模拟发送事件。

测试结果为:

(3)优化

在上面的例子中,想要发送事件,必须手动把处理器注册到监听器中。现在尝试优化一下,用配置的方式注册处理器。

1.写一个工具类

PropertiesUtils.java

PropertiesUtils工具类用于获取配置文件中的内容。

2.写出配置文件

config.properties

这个配置文件中使用observers作为key,处理器的全类名作为value。监听器将调用PropertiesUtils工具类,获取配置文件中的全类名,注册相应的处理器。

3.注入

修改事件监听类,写一个构造方法,注入处理器类。

EventListener.java

修改测试类,不需要再手动注入:

Mock.java

实际上,这种利用配置文件进行优化的方式并不够优雅,完全可以使用spring代替。有兴趣的同学可以自行尝试一下。

增加文章后,只要推送一个事件,处理器就会做相应的处理(邮件、积分、索引),实现了主逻辑和辅助逻辑的解耦。

三、总结

使用事件就是为了解耦。下次我将使用事件的方式实现一个常见的业务场景。

发布者

xie4ever

发表评论

电子邮件地址不会被公开。