Java 设计模式实战:Event Aggregator(事件聚合器)模式深度解析 —— 基于 java-design-patterns 开源仓库
示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载Event Aggregator事件聚合器是一种结构型设计模式用于解决“多个事件源 多个事件类型”场景下客户端订阅逻辑爆炸的问题它将众多事件源的事件注册与分发集中到一个聚合器对象上客户端只需面向聚合器注册一次即可接收来自所有源头的事件。本文以 java-design-patterns 仓库中的 event-aggregator 模块 为蓝本结合完整源码、测试用例与运行输出讲解该模式的核心思想、参与者角色、编程式实现、事件流转时序与适用场景让读者既能看懂模式原理也能直接照抄示例落地到自己的事件驱动系统中。一、模式动机为什么需要事件聚合器当一个系统中有大量对象都可能产生事件时客户端若想订阅这些事件会面临两个层面的复杂度发现成本高客户端必须逐个找到所有事件源并分别完成注册订阅数量爆炸如果每个事件源还暴露多个事件类型那么每个事件都要单独订阅一次注册代码量随“源数量 × 事件数量”线性膨胀。Event Aggregator 的解决思路是让聚合器成为众多事件源的“单一事件出口”。聚合器自身向所有事件源注册客户端只面向聚合器注册从而把复杂的多点订阅收敛为单点订阅。这种收拢同时带来一个附带收益当观察者数量众多、生命周期复杂时聚合器也简化了观察者的内存管理问题取消注册只发生在一处。二、理解模式的一个经典类比原文档用一个《冰与火之歌》式的隐喻来解释该模式见 localization/es/event-aggregator/README.md国王 Joffrey 坐在铁王座上统治七大王国他接收的大部分关键情报都来自“国王之手”Hand of the King即二把手。而国王之手身边有许多亲近的顾问这些顾问持续向他汇报王国内发生的事件。在这个类比中国王是最终消费者观察者国王之手是事件聚合器众多顾问情报源是事件生产者。国王不需要逐个联系所有顾问只需与国王之手保持沟通国王之手负责汇总各路情报并转呈国王。一句话概括该模式事件聚合器是一个事件中介它收集来自多个源的事件并分发给已注册的观察者。三、参与者与角色划分从源码结构看event-aggregator/src/main/java/com/iluwatar/event/aggregator该模块包含 10 个类/枚举角色划分如下角色类名职责观察者接口EventObserver定义事件消费契约只有一个方法onEvent(Event e)事件发射器基类EventEmitter维护Event → 观察者列表的注册表提供注册与通知的通用机制事件类型枚举Event定义系统内全部业务事件WHITE_WALKERS_SIGHTED、STARK_SIGHTED、WARSHIPS_APPROACHING、TRAITOR_DETECTED时间驱动枚举Weekday定义周一至周日作为事件发射的“时钟”聚合器KingsHand既是观察者订阅下属的事件又是发射器把事件转呈给国王承担事件中转最终观察者KingJoffrey只订阅国王之手这一个聚合器消费所有事件事件源LordBaelish、LordVarys、Scout各自在特定星期发射特定事件角色之间的关系可以概括为所有事件源LordBaelish、LordVarys、Scout把事件发给KingsHandKingsHand聚合器再把事件统一转发给KingJoffrey除Scout外多数参与者既实现了EventObserver接口又继承了EventEmitter基类——这正体现了聚合器“既是消费者又是生产者”的双重身份。四、核心机制源码剖析4.1 观察者契约EventObserverEventObserver.java 是全模式最精简的接口只声明一个方法public interface EventObserver { void onEvent(Event e); }所有希望接收事件的类都必须实现它。在示例中KingJoffrey用它打印收到的情报Slf4j public class KingJoffrey implements EventObserver { Override public void onEvent(Event e) { LOGGER.info(Received event from the Kings Hand: {}, e.toString()); } }4.2 事件定义Event 与 WeekdayEvent.java 用枚举固定了系统的全部事件类型并通过toString()返回可读描述RequiredArgsConstructor public enum Event { WHITE_WALKERS_SIGHTED(White walkers sighted), STARK_SIGHTED(Stark sighted), WARSHIPS_APPROACHING(Warships approaching), TRAITOR_DETECTED(Traitor detected); private final String description; Override public String toString() { return description; } }Weekday.java 同样是一个带描述文本的枚举定义了MONDAY到SUNDAY它作为时间推进的输入参数决定哪个事件源在“哪天”发事件。4.3 注册与通知的通用底座EventEmitterEventEmitter.java 是模式的核心机制所在。它用MapEvent, ListEventObserver维护“事件类型 → 观察者列表”的注册表并提供三个关键能力public abstract class EventEmitter { private final MapEvent, ListEventObserver observerLists; public EventEmitter() { observerLists new HashMap(); } // 便捷构造创建时即注册一个默认观察者 public EventEmitter(EventObserver obs, Event e) { this(); registerObserver(obs, e); } // 按事件类型注册观察者自动初始化列表并去重 public final void registerObserver(EventObserver obs, Event e) { if (!observerLists.containsKey(e)) { observerLists.put(e, new LinkedList()); } if (!observerLists.get(e).contains(obs)) { observerLists.get(e).add(obs); } } // 通知指定事件的所有观察者 protected void notifyObservers(Event e) { if (observerLists.containsKey(e)) { observerLists.get(e).forEach(observer - observer.onEvent(e)); } } // 由子类实现时间推进时的“事件触发点” public abstract void timePasses(Weekday day); }值得注意的实现细节按事件粒度注册注册表以Event为键意味着同一个观察者可以针对不同事件被多次注册也意味着同一个事件可以有多个观察者形成事件维度的多对多关系去重保护registerObserver在加入前用contains(obs)检查避免同一观察者对同一事件重复注册导致重复通知空安全通知notifyObservers先检查containsKey(e)对未注册事件静默跳过不会抛异常final修饰注册方法注册逻辑被固化在基类子类只能通过重写timePasses和onEvent表达业务差异保证了注册机制的稳定统一。4.4 聚合器实现KingsHandKingsHand.java 是模式的“枢纽节点”。它同时继承EventEmitter作为发射器并实现EventObserver作为观察者其onEvent方法直接把收到的事件原样转发给下游注册者public class KingsHand extends EventEmitter implements EventObserver { public KingsHand() {} public KingsHand(EventObserver obs, Event e) { super(obs, e); } Override public void onEvent(Event e) { notifyObservers(e); } Override public void timePasses(Weekday day) { // 空实现聚合器不直接产生事件只做中转 } }从源码结构看KingsHand的onEvent就是整条事件链路的“转发点”上游事件源调用它的onEvent它随即调用notifyObservers把事件派发给下游观察者。而它的timePasses被有意留空并在注释中说明“聚合器不直接处理基于时间的事件只是为满足接口契约而占位”——这从反面印证了聚合器“只中转、不生产”的角色定位。五、事件源实现谁在什么时候发什么事件三个事件源类都继承EventEmitter通过重写timePasses(Weekday)实现“特定星期触发特定事件”的调度逻辑LordVarysLordVarys.java——每周六发现内奸Override public void timePasses(Weekday day) { if (day Weekday.SATURDAY) { notifyObservers(Event.TRAITOR_DETECTED); } }ScoutScout.java——周二发现战船逼近、周三发现异鬼Override public void timePasses(Weekday day) { if (day Weekday.TUESDAY) { notifyObservers(Event.WARSHIPS_APPROACHING); } if (day Weekday.WEDNESDAY) { notifyObservers(Event.WHITE_WALKERS_SIGHTED); } }LordBaelishLordBaelish.java——每周五发现史塔克家族踪迹Override public void timePasses(Weekday day) { if (day Weekday.FRIDAY) { notifyObservers(Event.STARK_SIGHTED); } }可见事件源的公共模式是在timePasses内判断星期命中特定Weekday时调用notifyObservers(Event.XXX)广播事件。若子类也实现了EventObserver如LordVarys则同样可以在onEvent中继续转发形成多级链条。六、组装与运行完整示例拆解App.java 的main方法完整演示了如何搭建这条事件链。原文档给出了核心片段这里结合源码逐段解读var kingJoffrey new KingJoffrey(); var kingsHand new KingsHand(); kingsHand.registerObserver(kingJoffrey, Event.TRAITOR_DETECTED); kingsHand.registerObserver(kingJoffrey, Event.STARK_SIGHTED); kingsHand.registerObserver(kingJoffrey, Event.WARSHIPS_APPROACHING); kingsHand.registerObserver(kingJoffrey, Event.WHITE_WALKERS_SIGHTED); var varys new LordVarys(); varys.registerObserver(kingsHand, Event.TRAITOR_DETECTED); varys.registerObserver(kingsHand, Event.WHITE_WALKERS_SIGHTED); var scout new Scout(); scout.registerObserver(kingsHand, Event.WARSHIPS_APPROACHING); scout.registerObserver(varys, Event.WHITE_WALKERS_SIGHTED); var baelish new LordBaelish(kingsHand, Event.STARK_SIGHTED);这里需要强调的组装要点最终消费者只订阅聚合器KingJoffrey只与KingsHand建立注册关系对上游的LordBaelish、LordVarys、Scout一无所知这正是“面向单一出口订阅”的体现聚合器订阅所有事件源KingsHand分别注册到LordVarysTRAITOR_DETECTED、WHITE_WALKERS_SIGHTED、ScoutWARSHIPS_APPROACHING、LordBaelishSTARK_SIGHTED双参数构造器简化注册new LordBaelish(kingsHand, Event.STARK_SIGHTED)等价于先new LordBaelish()再调用registerObserver(kingsHand, Event.STARK_SIGHTED)利用了EventEmitter的便捷构造方法多级链条可以存在scout.registerObserver(varys, Event.WHITE_WALKERS_SIGHTED)让Scout的“异鬼事件”既直送KingsHand又经LordVarys转发展示了事件可以在观察者之间接力传播。驱动事件流转的核心循环使用流式 API把“每个发射器 × 每个星期”组合遍历var emitters List.of(kingsHand, baelish, varys, scout); Arrays.stream(Weekday.values()) .Consumer? super EventEmittermap(day - emitter - emitter.timePasses(day)) .forEachOrdered(emitters::forEach);这段代码的含义是依次取出周一至周日对emitters列表中的每个发射器调用timePasses(day)从而触发各事件源在其“专属日”广播事件再由KingsHand汇总转发给KingJoffrey。运行方式在仓库根目录执行 Maven 命令即可运行该模块./mvnw -pl event-aggregator compile exec:java -Dexec.mainClasscom.iluwatar.event.aggregator.App若本机已安装 Maven也可直接使用mvn替代./mvnw。运行输出原文档给出了示例运行后的控制台输出事件经聚合器统一转呈给国王18:21:52.955 [main] INFO com.iluwatar.event.aggregator.KingJoffrey - Received event from the Kings Hand: Warships approaching 18:21:52.960 [main] INFO com.iluwatar.event.aggregator.KingJoffrey - Received event from the Kings Hand: White walkers sighted 18:21:52.960 [main] INFO com.iluwatar.event.aggregator.KingJoffrey - Received event from the Kings Hand: Stark sighted 18:21:52.960 [main] INFO com.iluwatar.event.aggregator.KingJoffrey - Received event from the Kings Hand: Traitor detected四条日志全部以 “Received event from the Kings Hand” 开头直观证明最终消费者收到的每一条事件都来自唯一的聚合器KingsHand事件源与消费者之间没有直接耦合。七、事件流转时序结合 event-aggregator/etc/event-aggregator-sequence-diagram.png 与源码调用关系一次完整的事件流转可概括为四个阶段触发App.main遍历Weekday对每个发射器调用timePasses(day)广播事件源如周六的LordVarys在timePasses内命中条件调用notifyObservers(Event.TRAITOR_DETECTED)聚合转发注册表中该事件的观察者KingsHand收到onEvent(...)调用立即再次调用自己的notifyObservers(e)消费KingsHand的下游观察者KingJoffrey的onEvent被触发打印日志完成消费。整个过程是“生产者 → 聚合器 → 消费者”的两段式传递聚合器是唯一的中间人。八、类结构总览上图来源 event-aggregator/etc/classes.png展示了该模块的完整静态结构EventEmitter作为抽象基类与EventObserver接口之间存在“维护 0..* 个观察者”的关联承担注册与通知KingsHand、LordVarys等业务类继承EventEmitter其中KingsHand、LordVarys还实现EventObserver接口兼具观察者身份Event枚举定义四种业务事件Weekday枚举提供时间上下文二者共同支撑“时间驱动的多源事件分发”。九、测试验证行为如何被保证模块内置的测试验证了模式行为的正确性见 event-aggregator/src/test/java/com/iluwatar/event/aggregator。核心测试基类 EventEmitterTest.java 用 Mockito 对观察者打桩逐一验证专属日触发对Weekday.values()全周遍历依次调用emitter.timePasses(weekday)在specialDay当天断言每个注册观察者都恰好收到一次onEvent(event)verify(observer, times(1)).onEvent(eq(event))非专属日静默其他日期断言观察者零交互verifyNoMoreInteractions(observers)确保事件不会错发两种注册路径等价测试同时覆盖“无默认观察者 手动 registerObserver”与“带默认观察者的双参构造器”两条注册路径保证便捷构造与手动注册行为一致。此外AppTest.java、EventTest.java、WeekdayTest.java 等测试分别覆盖入口执行、枚举描述文本与toString()行为构成对模式的完整行为验证。十、适用场景与使用建议原文档给出的适用性判断是当系统中存在大量潜在事件源时Event Aggregator 是很好的选择。与其让观察者逐个注册所有事件源不如把注册逻辑集中到聚合器。除简化注册外聚合器还简化了观察者使用中的内存管理问题。结合源码可以进一步推导出适合该模式的特征事件源数量多、事件类型多且客户端只关心最终结果不关心具体来源需要为“N 个事件源 × M 个事件类型”提供统一的订阅入口降低客户端注册代码量需要一处集中管理观察者的注册与注销避免观察者散落各事件源导致的内存泄漏风险希望事件能在观察者之间接力传递如Scout → LordVarys → KingsHand → KingJoffrey时聚合器天然支持多级转发。需要注意的边界基于源码结构推断EventEmitter的注册表是普通的HashMapLinkedList未做并发同步notifyObservers采用同步顺序遍历。因此该实现适用于单线程或对实时性要求不高的场景若在多线程环境中使用需要自行补充线程安全措施。十一、与相关模式的对比原文档明确指出 Event Aggregator 与Observer观察者模式紧密相关从结构上看Event Aggregator 内部正是借助观察者机制EventObserver接口、注册、通知实现的。二者的差异在于关注点Observer解决“一个对象状态变化时如何通知多个依赖对象”的基本通信问题关注点在一对多通知本身Event Aggregator解决“多个事件源 多个事件类型如何被统一订阅”的编排问题关注点在于把多点订阅收敛为单点订阅可视为对观察者机制在“多源汇聚”方向上的组合应用。十二、总结Event Aggregator 用“一个中间人”化解了多事件源订阅的复杂度事件源只需把事件交给聚合器消费者只需向聚合器注册中间的事件路由、注册管理全部收敛到一个对象。通过本模块源码可以看到其完整落地方案——EventEmitter基类提供按事件粒度的注册表与通知机制KingsHand作为聚合器承担转发KingJoffrey作为纯消费者只依赖唯一事件出口。如果你正在设计事件驱动的客户端订阅层且面临“源多、事杂、订阅乱”的问题Event Aggregator 是一个值得优先考虑的结构型方案仓库中的 event-aggregator 模块 提供了可直接复用的参考实现。参考资料模块入口文档event-aggregator/README.md西班牙语版原始文档localization/es/event-aggregator/README.md核心源码目录event-aggregator/src/main/java/com/iluwatar/event/aggregator测试源码目录event-aggregator/src/test/java/com/iluwatar/event/aggregator类图与时序图event-aggregator/etc/classes.png、event-aggregator/etc/event-aggregator-sequence-diagram.png赞分享示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载相关推荐深入解析 Java 设计模式Event Aggregator 事件聚合器模式在 java-design-patterns 中的实现与应用深入解析 Java 设计模式Event Aggregator 事件聚合器模式在 java design patterns 中的实现与应用 事件聚合器Even示例工程教程java-design-patterns 之 Event Aggregator事件聚合器模式集中式事件管理的完整实战指南java design patterns 之 Event Aggregator事件聚合器模式集中式事件管理的完整实战指南 Event Aggregator示例工程教程Java 设计模式Front Controller 前端控制器模式深度解析基于 java-design-patterns 仓库Java 设计模式Front Controller 前端控制器模式深度解析基于 java design patterns 仓库 前端控制器Front C示例工程教程上一篇猫抓浏览器扩展三分钟掌握网页视频下载的终极解决方案下一篇RtspClientSharp高级教程TCP与UDP传输模式切换及性能优化技巧创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →