再谈领域事件

DD的博客全面升级,阅读体验更佳(尤其是系列教程),后续不再通过这里发布新文章,而是改到 www.didispace.com 发布啦,奔走相告!点击直达~

我以前写过一篇关于领域事件的文章——实现领域事件,随着在项目中深入的使用DDD架构,我对领域事件有了新的认识。尤其是采用领域事件来解耦代码这种方式对项目的发展具有深远的影响。

我在实现领域事件中主要谈到了如何在技术层面去实现发布事件与订阅事件,比较了几种不同的方式以及它们背后的原理。但随着我在自己负责的项目中严格地实施DDD架构时,我发现如何去发布订阅领域事件的意义远没有决定去做这件事情本身重要。换句话说,与其纠结与是使用基于Spring的事件架构还是Guava提供的EventBus,是使用同步发布还是异步发布,还不如想想去做这件事情对你的项目会产生怎样的影响。

为什么要使用事件?我认为这是所有人应该考虑的首要问题。对我来说,使用事件的意义有两个方面,一是在于流程上的解耦,二是在于代码层面的解耦。在代码层面的解耦是显而易见的,我就不再赘述了。那么流程上的解耦是什么意思了?我们先看一下一个普通的业务流程执行的链路。

正常的业务流程

目前我们绝大部分人的思维习惯是顺序式的,体现在代码上也就是A做完它做的事情然后B继续处理,当然这么做没有任何问题,这也是最为简单直观的一种编程方式。我们再来看一下通过Event来解耦的链路。

通过事件来解耦

通过引入事件,我们将过程A和过程B解耦了。第一种方式和第二种方式都有着其重要的存在意义,决定何时采用第二种方式的关键在于BoundedContext。正好最近我在负责处理一个遗留系统的拆分问题,恰好有一个好的例子来说明这个问题。

这个遗留系统是一个计费系统,因为各种各样的原因,整个项目在代码层面非常混乱,代码之间各种凌乱的引用和交叉,这种感觉就和下图一样。

一团乱麻.jpg

我认为造成这个问题的根源在于开发人员并没有及时地识别出这个项目中的几个关键领域以及及早的将其进行隔离。更为让人遗憾的是开发这个项目的人员都已离职,后来接手这个项目的开发人员被堆积地需求压得喘不过气来,也就更没有时间来处理以前的技术债务问题。

实际上,这个项目包含多个领域,最为核心的三个领域就是订单、账单和计费。在和老大以及开发沟通过后,我们意识到系统拆分已经刻不容缓。目前我们在做的事情就是在工程内部进行代码级别的拆分,其中最为棘手的问题就是订单系统和计费系统的耦合太深。

仔细分析各个业务流程之后我们发现,很多耦合都是可以避免的。大部分的业务流程都是由订单系统触发,然后计费系统做出相应的变更。最终,我们决定使用领域事件来讲订单系统和计费系统解耦开。(PS:原系统中并没有使用DDD的开发模式,但这并不影响我们使用领域事件。)

order-bill

上图是我们现在的做法,通过OrderEventBillEvent来将两个系统解耦开,然后将Event放到一个公共的Module中来达到Module级别的解耦。令人惊喜的发现在于,这种解耦的方式与我们规划中订单系统与计费系统通过MQ来通信达成了一致。后面我们只需要标准化这些事件,就可以做到无缝迁移到MQ中。

通过上面这个例子,我再总结一下使用领域事件的来解耦业务流程的应用场景:

  1. 如果一个业务流程需要贯穿几个不同的受限上下文中,那么可以通过以发布领域事件的方式来避免上游系统耦合下游系统。这种解耦方式收益最大,因为其有利于后期系统间的拆分。
  2. 如果在同一个受限上下文中,也可以通过发布领域事件的方式来达到领域间解耦。

至于为什么说以何种方式来发布事件不在那么重要,因为当你在项目采用了领域事件技术来解耦代码,你已经获得这项技术的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!

本文作者:Michael-J,
原文链接:http://michael-j.net/2017/08/13/再谈领域事件/
版权归作者所有,转载请注明作者、原文、译者等出处信息