再谈领域事件
我以前写过一篇关于领域事件的文章——《实现领域事件》,随着在项目中深入的使用DDD架构,我对领域事件有了新的认识。尤其是采用领域事件来解耦代码这种方式对项目的发展具有深远的影响。
我在《实现领域事件》中主要谈到了如何在技术层面去实现发布事件与订阅事件,比较了几种不同的方式以及它们背后的原理。但随着我在自己负责的项目中严格地实施DDD架构时,我发现如何去发布订阅领域事件的意义远没有决定去做这件事情本身重要。换句话说,与其纠结与是使用基于 Spring
的事件架构还是 Guava
提供的 EventBus
,是使用同步发布还是异步发布,还不如想想去做这件事情对你的项目会产生怎样的影响。
为什么要使用事件?我认为这是所有人应该考虑的首要问题。对我来说,使用事件的意义有两个方面,一是在于流程上的解耦,二是在于代码层面的解耦。在代码层面的解耦是显而易见的,我就不再赘述了。那么流程上的解耦是什么意思了?我们先看一下一个普通的业务流程执行的链路。
目前我们绝大部分人的思维习惯是顺序式的,体现在代码上也就是 A
做完它做的事情然后 B
继续处理,当然这么做没有任何问题,这也是最为简单直观的一种编程方式。我们再来看一下通过 Event
来解耦的链路。
通过引入事件,我们将过程A和过程B解耦了。第一种方式和第二种方式都有着其重要的存在意义,决定何时采用第二种方式的关键在于BoundedContext。正好最近我在负责处理一个遗留系统的拆分问题,恰好有一个好的例子来说明这个问题。
这个遗留系统是一个计费系统,因为各种各样的原因,整个项目在代码层面非常混乱,代码之间各种凌乱的引用和交叉,这种感觉就和下图一样。
我认为造成这个问题的根源在于开发人员并没有及时地识别出这个项目中的几个关键领域以及及早的将其进行隔离。更为让人遗憾的是开发这个项目的人员都已离职,后来接手这个项目的开发人员被堆积地需求压得喘不过气来,也就更没有时间来处理以前的技术债务问题。
实际上,这个项目包含多个领域,最为核心的三个领域就是订单、账单和计费。在和老大以及开发沟通过后,我们意识到系统拆分已经刻不容缓。目前我们在做的事情就是在工程内部进行代码级别的拆分,其中最为棘手的问题就是订单系统和计费系统的耦合太深。
仔细分析各个业务流程之后我们发现,很多耦合都是可以避免的。大部分的业务流程都是由订单系统触发,然后计费系统做出相应的变更。最终,我们决定使用领域事件来讲订单系统和计费系统解耦开。(PS:原系统中并没有使用DDD的开发模式,但这并不影响我们使用领域事件。)
上图是我们现在的做法,通过 OrderEvent
和 BillEvent
来将两个系统解耦开,然后将 Event
放到一个公共的Module中来达到Module级别的解耦。令人惊喜的发现在于,这种解耦的方式与我们规划中订单系统与计费系统通过MQ来通信达成了一致。后面我们只需要标准化这些事件,就可以做到无缝迁移到MQ中。
通过上面这个例子,我再总结一下使用领域事件的来解耦业务流程的应用场景:
- 如果一个业务流程需要贯穿几个不同的受限上下文中,那么可以通过以发布领域事件的方式来避免上游系统耦合下游系统。这种解耦方式收益最大,因为其有利于后期系统间的拆分。
- 如果在同一个受限上下文中,也可以通过发布领域事件的方式来达到领域间解耦。
至于为什么说以何种方式来发布事件不在那么重要,因为当你在项目采用了领域事件技术来解耦代码,你已经获得这项技术的90%的好处,而具体怎么执行就显得不那么重要了。我在另外一个项目中(这个项目完全采用DDD的模式来开发)就采用了最为朴实的方式来实现,不再基于Spring或者Guava了。
附上我目前的使用方法:
/**
* 领域事件
* Created by jiangwenkang on 16-11-17.
*/
public interface DomainEvent extends Serializable {
Date occurredTime();}
/**
* 领域事件发布器
* Created by jiangwenkang on 16-11-17.
*/
public class DomainEventPublisher {
private static ConcurrentHashMap<Class<? extends DomainEvent>, List<DomainEventSubscriber<? extends DomainEvent>>> subscriberMap = new ConcurrentHashMap<>();
public synchronized static <T extends DomainEvent> void subscribe(Class<T> domainEventClazz, DomainEventSubscriber<T> subscriber) {
List<DomainEventSubscriber<? extends DomainEvent>> domainEventSubscribers = subscriberMap.get(domainEventClazz);
if (domainEventSubscribers == null) {
domainEventSubscribers = Lists.newArrayList();
}
domainEventSubscribers.add(subscriber);
subscriberMap.put(domainEventClazz, domainEventSubscribers);
}
@SuppressWarnings("unchecked")
public static <T extends DomainEvent> void publish(final T domainEvent) {
if (domainEvent == null) {
throw new IllegalArgumentException("domain event is null");
}
List<DomainEventSubscriber<? extends DomainEvent>> subscribers = subscriberMap.get(domainEvent.getClass());
if (subscribers != null && !subscribers.isEmpty()) {
for (DomainEventSubscriber subscriber : subscribers) {
subscriber.handle(domainEvent);
}
}
}
}
/**
* 领域事件订阅器
* Created by jiangwenkang on 16-11-17.
*/
public interface DomainEventSubscriber<T extends DomainEvent> {
/**
* 订阅者处理事件 *
* @param event 领域事件
*/
void handle(T event);
}
Github仓库:https://gist.github.com/mymonkey110/aba58de452928bec2243848bb2c9b84a
如果你对使用领域事件的感触没有那么深,那么请记住这句话:代码间解耦用事件,系统间解耦用MQ!
- 摸金Redis漏洞
- 机器人越来越像人,你会担心你的工作被人工智能取代吗?
- 一句代码实现批量数据绑定[上篇]
- 机器学习-从高频号码中预测出快递送餐与广告骚扰
- MS Windows 下基于Atom的LaTeX编译环境的配置
- WCF中的Binding模型之一: Binding模型简介
- WCF中的Binding模型之一: Binding模型简介
- 2017最火的五篇深度学习论文 总有一篇适合你
- SplashScreenSource的妙用
- SplashScreenSource的妙用
- SplashScreenSource的妙用
- Nodejs学习笔记(十七)--- 浮点运算decimal.js
- AI时代让自己幸福更是一种能力
- 持续不断地推荐儿童不宜视频背后,YouTube是这样训练AI的
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 10 | Tornado源码分析:Gen 对象(上)
- springboot使用spring-cloud-starter-alibaba-sentinel导致响应变成xml格式
- 内网安全攻防之内网渗透测试基础
- CMake脚本中如何修改XCode工程属性?
- 数值微分|有限差分法的误差分析
- Odyssey
- 【MySQL】之join算法详解
- 性能最佳实践:查询模式和分析
- Node 如何在 Controller 层进行数据校验
- FlutterDojo设计之道——状态管理之路(二)
- EyouCms前台GetShell漏洞复现
- CentOS7系统使用rpm方式安装MySQL5.7
- Mysql的主从复制
- 初学者指南:什么是算法?11行伪代码给你讲明白
- Shiro安全框架入门学习