网易首页 > 网易号 > 正文 申请入驻

Guava、Spring 如何抽象观察者模式?

0
分享至

  【CSDN 编者按】总结:灵活解耦、业务分离、单一责任、易维护,那么就可以使用观察者模式了。

  作者 | 龙台 责编 | 欧阳姝黎

  前言

  《设计模式实战》系列目前已经写了 7 篇了,最近几篇平均阅读保持 1.1k+,后面也会延续之前的高质量,欢迎继续关注

  •   【设计模式实战】策略模式

  •   【设计模式实战】责任链模式

  •   【设计模式实战】Builder 模式

  •   【设计模式实战】代理模式

  •   【设计模式实战】模版方法模式

  •   【设计模式实战】适配器模式

  •   【设计模式实战】...

  今天讲解一篇行为型设计模式,什么是行为型?行为型主要负责设计类或对象之间的交互。工作中常用的观察者模式就是一种行为型设计模式

  最近在尝试重构之前写过的代码。在重新梳理过业务之后,发现已有的设计场景应该能够接入到设计模式,而且查看了代码的提交记录,更是坚定了此想法

  保持之前的一贯作风,想要说明一个设计模式,需要三板斧支撑。什么是观察者模式?如何使用观察者模式?项目中应该如何应用?

观察者设计模式大纲如下: 什么是观察者模式 观察者模式代码如何写 如何使用观察者模式结合业务 Guava EventBus 观察者模式 Spring ApplicationEvent 事件模型 观察者模式最后的总结

  什么是观察者模式

  观察者模式是一种行为设计模式,允许定义一种订阅通知机制,可以在对象(被观察者)事件发生时通知多个 “观察” 该对象的观察者对象,所以也被称为发布订阅模式

  其实我个人而言,不太喜欢使用文字去定义一种设计模式的语义,因为这样总是难以理解。所以就有了下面生活中的例子,来帮助读者更好的去理解模式的语义。类图如下所示:

  在举例说明前,先让我们熟悉下观察者模式中的 角色类型 以及代码示例。观察者模式由以下几部分角色组成,可以参考代码示例去理解,不要被文字描述带偏

  •   主题(被观察者)(Subject):抽象主题角色把所有观察者对象保存在一个容器里,提供添加和移除观察者接口,并且提供出通知所有观察者对象接口(也有作者通过 Observable 描述)

  •   具体主题(具体被观察者)(Concrete Subject):具体主题角色的职责就是实现抽象目标角色的接口语义,在被观察者状态更改时,给容器内所有注册观察者发送状态通知

public interface Subject {
void register(Observer observer); // 添加观察者
void remove(Observer observer); // 移除观察者
void notify(String message); // 通知所有观察者事件

  public class ConcreteSubject implements Subject {
private static final List observers = new ArrayList();

  @Override
public void register(Observer observer) { observers.add(observer); }

  @Override
public void remove(Observer observer) { observers.remove(observer); }

  @Override
public void notify(String message) { observers.forEach(each -> each.update(message)); }
}

  •   抽象观察者(Observer):抽象观察者角色是观察者的行为抽象,它定义了一个修改接口,当被观察者发出事件时通知自己

  •   具体观察者(Concrete Observer):实现抽象观察者定义的更新接口,可以在被观察者发出事件时通知自己

public interface Observer {
void update(String message); // String 入参只是举例, 真实业务不会限制

  public class ConcreteObserverOne implements Observer {
@Override
public void update(String message) {
// 执行 message 逻辑
System.out.println("接收到被观察者状态变更-1");
}
}

  public class ConcreteObserverTwo implements Observer {
@Override
public void update(String message) {
// 执行 message 逻辑
System.out.println("接收到被观察者状态变更-2");
}
}

  我们跑一下上面的观察者模式示例,如果不出意外的话会将两个观察者执行逻辑中的日志打印输出。如果是平常业务逻辑,抽象观察者定义的入参是具有业务意义的,大家可以类比项目上使用到的 MQ Message 机制

  public class Example {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();
subject.register(new ConcreteObserverOne());
subject.register(new ConcreteObserverTwo());
subject.notify("被观察者状态改变, 通知所有已注册观察者");

  
观察者模式结合业务

  因为公司业务场景保密,所以下面我们通过【新警察故 事】的电影情节,稍微篡改下剧情,模拟出我们的观察者模式应用场景

  假设:目前我们有三个警察,分别是龙哥、锋哥、老三,他们受命跟进犯罪嫌疑人阿祖。如果发现犯罪嫌疑人阿祖有动静,龙哥、峰哥负责实施抓捕行动,老三向警察局要人,流程图如下:

  如果说使用常规代码写这套流程,是能够实现需求的,一把梭的逻辑可以实现一切需求。但是,如果说下次行动,龙哥让老三跟着自己实施抓捕,亦或者说龙哥团队扩张,来了老四、老五、老六...

对比观察者模式角色定义,老四、老五、老六都是具体的观察者(Concrete Observer)

  如果按照上面的设想,我们通过“一把梭”的方式把代码写出来会有什么问题呢?如下:

  1.   首当其冲,增加了代码的复杂性。实现类或者说这个方法函数奇大无比,因为随着警员的扩张,代码块会越来越大

  2.   违背了开闭原则,因为会频繁改动不同警员的任务。每个警员的任务不是一成不变的,举个例子来说这次针对疑犯,让峰哥实施的抓捕行动,下次就可能是疏散民众,难道每次的更改都需要改动“一把梭”的代码

  第一种我们可以通过,大函数拆小函数或者大类拆分为小类的方式解决代码负责性问题。但是,开闭原则却不能避免掉,因为随着警员(观察者)的增多及减少,势必会面临频繁改动原函数的情况

  当我们面对这种已知会变动,并且可能会频繁变动不固定的代码,就要使用抽象思维来进行设计,进而保持代码的简洁、可维护

  这里使用 Java SpringBoot 项目结构来书写观察者模式,代码最终推送到 Github 仓库。读者可以先把仓库拉下来,因为其中不止示例代码,还包括 Guava 和 Spring 的观察者模式实现 GitHub 仓库地址

  首先,定义观察者模式中的观察者角色,分别为抽象观察者接口以及三个具体观察者实现类。实际业务中,设计模式会和 Spring 框架相结合,所以示例代码中包含 Spring 相关注解及接口

  其次,定义抽象被观察者接口以及具体被观察者实现类。同上,被观察者也需要成为 Spring Bean,托管于 IOC 容器管理

  到这里,一个完整的观察者模式就完成了。但是,细心的读者会发现这样的观察者模式会有一个小问题,这里先不说明,继续往下看。接下来就需要实际操练一番,注册这些观察者,通过被观察者触发事件来通知观察者

  看了应用的代码之后,函数体过大的问题已经被解决了,我们通过拆分成为不同的具体的观察者类来拆分总体逻辑。但是开闭原则问题呢?这就是上面所说的问题所在,我们目前是通过显示的引入具体观察者模式来进行添加到被观察者的通知容器中,如果后续添加警察老四、老五... 越来越多的警察时,还是需要改动原有代码,问题应该怎么解决呢

  其实非常简单,平常 Web 项目基本都会使用 Spring 框架开发,那自然是要运用其中的特性解决场景问题。我们这里通过改造具体被观察者实现开闭原则

  如果看过之前作者写过的设计模式文章,对 InitializingBean 接口不会感到陌生,我们在 afterPropertiesSet 方法中,通过注入的IOC 容器获取到所有观察者对象并添加至被观察者通知容器中。这样的话,触发观察者事件,代码中只需要一行即可完成通知

  @PostConstruct
public void executor() {
// 被观察者触发事件, 通知所有观察者
subject.notify("阿祖有行动!");

  后续如果再有新的观察者类添加,只需要创建新的类实现抽象观察者接口即可完成需求。有时候,能够被封装起来的不只是 DateUtil 类型的工具类,一些设计模式也可以被封装,继而更好的服务开发者灵活运用。这里会分别介绍 Guava#EventBus 以及 Spring#事件模型

  同步异步的概念

  在介绍 EventBus 和 Spring 事件模型之前,有一道绕不过去的弯,那就是同步执行、异步执行的概念,以及在什么样的场景下使用同步、异步模型?

  •   同步执行:所谓同步执行,指的就是在发出一个请求后,在没有获得调用结果之前,调用者就会等待在当前代码。直到获取到调用方法的执行结果,才算是结束。总结一句话就是由调用者主动等待这个调用的结果,未返回之前不执行别的操作

  •   异步执行:而异步执行恰恰相反,发出调用请求后立即返回,并向下执行代码。异步调用方法一般不会有返回结果,调用之后就可以执行别的操作,一般通过回调函数的方式通知调用者结果

  这里给大家举个例子,能够很好的反应同步、异步的概念。比如说你想要给体检医院打电话预约体检,你说出自己想要预约的时间后,对面的小姐姐说:“稍等,我查一下时间是否可以”,这个时候如果你不挂电话,等着小姐姐查完告诉你之后才挂断电话,那这就是同步。如果她说稍等需要查一下,你告诉她:“我先挂了,查到结果后再打过来”,那这就是异步+回调

  在我们上面写的示例代码上,毋庸置疑是通过同步的形式执行观察者模式,那是否可以通过异步的方式执行观察者行为?答案当然是可以。我们可以通过在观察者模式行为执行前创建一个线程,那自然就是异步的。当然,不太建议你这么做,这样可能会牵扯出更多的问题。一起来看下 Guava 和 Spring 是如何封装观察者模式

  
Guava EventBus 解析

  EventBus 是 Google Guava 提供的消息发布-订阅类库,是设计模式中的观察者模式(生产/消费者模型)的经典实现

  具体代码已上传 GitHub 代码仓库,EventBus 实现中包含同步、异步两种方式,代码库中由同步方式实现观察者模式

  因为 EventBus 并不是文章重点,所以这里只会对其原理进行探讨。首先 EventBus 是一个同步类库,如果需要使用异步的,那就创建时候指定 AsyncEventBus

  // 创建同步 EventBus
EventBus eventBus = new EventBus();

  // 创建异步 AsyncEventBus
EventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(10));

  注意一点,创建 AsyncEventBus 需要指定线程池,其内部并没有默认指定。当然也别像上面代码直接用 Executors 创建,作者是为了图省事,如果从规范而言,还是消停的使用默认线程池构建方法创建 new ThreadPoolExecutor(xxx);

  EventBus 同步实现有一个比较有意思的点。观察者操作同步、异步行为时,均使用 Executor 去执行观察者内部代码,那如何保证 Executor 能同步执行呢。Guava 是这么做的:实现 Executor 接口,重写执行方法,调用 run 方法

  enum DirectExecutor implements Executor {
INSTANCE;

  @Override
public void execute(Runnable command) {
command.run();
}
}

  大家有兴趣可以去看下 EventBus 源码,不是很难理解,工作使用上还是挺方便的。只不过也有不好的地方,因为 EventBus 属于进程内操作,如果使用异步 AsyncEventBus 执行业务,存在丢失任务的可能

  
Spring 事件模型

  Spring 大拿设计的观察者模式抽象是作者看到的最优雅、最功能的设计,如果想要玩耍观察者模式推荐指数

  如果想要使用 ApplicationEvent 玩转观察者模式,只需要简单几步。总结:操作简单,功能强大

  1.   创建业务相关的 MyEvent,需要继承 ApplicationEvent,重写有参构造函数

  2.   定义不同的监听器(观察者)比如 ListenerOne 实现 ApplicationListener 接口,重写 onApplicationEvent 方法

  3.   通过 ApplicationContext#publishEvent 方法发布具体事件

  Spring 事件与 Guava EventBus 一样,代码就不粘贴了,都已经存放到 Github 代码仓库。这里重点介绍下 Spring 事件模型的特点,以及使用事项

  Spring 事件同样支持异步编程,需要在具体 Listener 实现类上添加 @Async 注解。支持 Listener 的订阅顺序,比如说有 A、B、C 三个 Listener。可以通过 @Order 注解实现多个观察者顺序消费

  作者建议读者朋友一定要跑下 ApplicationEvent 的 Demo,在使用框架的同时也要合理的运用框架提供的工具轮子,因为被框架封装出的功能,一般而言要比自己写的功能更强大、出现问题的几率更少。同时,切记不要重复造轮子,除非功能点不满足的情况下,可以借鉴原有轮子的基础上开发自己功能

  
结言

  文章通过图文并茂的方式帮助大家梳理了下观察者模式的实现方式,更是推出了进阶版的 EventBus 以及 ApplicationEvent,相信大家看完之后可以很愉快的在自己项目中玩耍设计模式了。切记哈,要在合理的场景下使用模式,一般而言观察者模式作用于观察者与被观察者之间的解耦合

  最后解答下最早提到的问题,项目中的观察者模式应该使用同步模型还是异步模型呢

  如果只是使用观察者模式拆分代码使其满足开闭原则、高内聚低耦合、职责单一 等特性,那么自然是使用同步去做,因为这种方式是最为稳妥。而如果 不关心观察者执行结果或者考虑性能 等情况,则可以使用异步的方式,通过回调的方式满足业务返回需求

  关于观察者设计模式本文就讲到这里,后面会陆续输出工厂、原型、享元等模式;如果文章对你有帮助那就点个关注支持下吧,祝好。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
WTT曝出大冷门,首个出局大种子选手诞生,印度怪球手晋级

WTT曝出大冷门,首个出局大种子选手诞生,印度怪球手晋级

极度说球
2026-05-24 13:31:44
孔蒂:一个月前我就告诉德佬我决定离开,一些环境我无法改变

孔蒂:一个月前我就告诉德佬我决定离开,一些环境我无法改变

懂球帝
2026-05-25 03:34:14
死了得了,我才22岁凭啥扛!大连宝妈带2岁娃送餐崩溃,丈夫躺平

死了得了,我才22岁凭啥扛!大连宝妈带2岁娃送餐崩溃,丈夫躺平

冷月侃娱乐
2026-05-21 17:40:53
纪委大数据有多厉害?这几类行为一查一个准,公职人员别再侥幸

纪委大数据有多厉害?这几类行为一查一个准,公职人员别再侥幸

细说职场
2026-05-18 09:37:45
媒体称美伊下一轮会谈可能在6月5日举行

媒体称美伊下一轮会谈可能在6月5日举行

界面新闻
2026-05-24 16:37:04
钱多有啥用?贝克汉姆身家超100亿,4个孩子却一个比一个让人揪心

钱多有啥用?贝克汉姆身家超100亿,4个孩子却一个比一个让人揪心

小椰的奶奶
2026-05-25 03:21:03
一脸憔悴、表情硬挤,却硬要在古装剧中演权臣,网友:男主成败笔

一脸憔悴、表情硬挤,却硬要在古装剧中演权臣,网友:男主成败笔

一娱三分地
2026-05-04 15:35:47
太难了!中大型SUV也迎来降价潮:最大让利41%,豪华SUV成重灾区

太难了!中大型SUV也迎来降价潮:最大让利41%,豪华SUV成重灾区

沙雕小琳琳
2026-05-24 22:14:45
越扒瓜越大!交大女学生吞奖金再添猛料,不止想进体制内这么简单

越扒瓜越大!交大女学生吞奖金再添猛料,不止想进体制内这么简单

观史搜寻着
2026-05-21 05:41:15
世界杯落选球星榜单出炉!两大亿元球星领衔,英格兰六大球星缺席

世界杯落选球星榜单出炉!两大亿元球星领衔,英格兰六大球星缺席

郝小小看体育
2026-05-23 21:51:50
基辅遭大规模弹道导弹袭击;乌再次打击“埃森海军上将”号护卫舰

基辅遭大规模弹道导弹袭击;乌再次打击“埃森海军上将”号护卫舰

近距离
2026-05-24 08:12:48
60岁阿姨疯狂买买买,还专门租房用来放快递:它们不会主动离开我

60岁阿姨疯狂买买买,还专门租房用来放快递:它们不会主动离开我

澎湃新闻
2026-05-24 19:05:02
两匹马受惊闯入小区 5岁女童被缰绳绕颈拖行近2公里身亡 女童妈妈:跑不过马,好绝望

两匹马受惊闯入小区 5岁女童被缰绳绕颈拖行近2公里身亡 女童妈妈:跑不过马,好绝望

闪电新闻
2026-05-24 12:38:27
关系没变糟,但你们已经远了

关系没变糟,但你们已经远了

心事寄山海
2026-05-24 01:02:48
中超积分榜变动大!上海申花1-2爆冷 辽宁5-0升第8 山东泰山遭绝

中超积分榜变动大!上海申花1-2爆冷 辽宁5-0升第8 山东泰山遭绝

郝小小看体育
2026-05-24 22:07:29
导演只是请窦骁来演个纨绔子弟,不想他一出场,翟子路20集白演了

导演只是请窦骁来演个纨绔子弟,不想他一出场,翟子路20集白演了

童叔不飙车
2026-05-22 15:45:45
歼35采购计划出现变动,巴基斯坦多方考量,持续对比多款西式战机

歼35采购计划出现变动,巴基斯坦多方考量,持续对比多款西式战机

趣味八卦
2026-05-24 14:40:23
别再被隋唐演义误导,正史中公认武力值最高的,是这五位顶尖猛将

别再被隋唐演义误导,正史中公认武力值最高的,是这五位顶尖猛将

长风文史
2026-05-21 17:01:36
周鹏任中共华容县委书记

周鹏任中共华容县委书记

湖南法治报
2026-05-24 16:51:14
终于知道苹果直营店的员工每天都很有活力的原因了,网友:门槛高

终于知道苹果直营店的员工每天都很有活力的原因了,网友:门槛高

另子维爱读史
2026-05-23 08:00:45
2026-05-25 06:15:01
CSDN incentive-icons
CSDN
成就一亿技术人
26571文章数 242291关注度
往期回顾 全部

科技要闻

我戴着摄像头上班,正在帮AI抢走我饭碗

头条要闻

山西矿难遇难者家属:父亲年过半百 我们一直劝他别干了

头条要闻

山西矿难遇难者家属:父亲年过半百 我们一直劝他别干了

体育要闻

唐斯发牌,大头逆袭:骑士跌向残忍夏季

娱乐要闻

王鹤棣掉粉超20万!代言和作品遭抵制

财经要闻

什么情况下,本轮AI大行情会结束?

汽车要闻

国民家轿再上新 帝豪向上系列限时5.59万起

态度原创

本地
艺术
数码
手机
公开课

本地新闻

用云锦的方式,打开江苏南京

艺术要闻

他把葡萄画成了美少女

数码要闻

618游戏本怎么选?ROG魔霸新锐2026来袭,福利秒杀让战力飞

手机要闻

为什么建议大家赶紧换新机?五点原因,望周知!

公开课

李玫瑾:为什么性格比能力更重要?

无障碍浏览 进入关怀版