当你的系统或者业务变得日益复杂时, DDD
的模式是一种非常值得尝试的架构模式。 DDD
让你更加关注于你的业务领域,思考你的业务模型,帮组你理清繁杂的业务关系。我推荐所有还没有了解过或者接触过 DDD
的后端工程师都去学习一下该架构模式。本文主要关注 DDD
中的 领域事件
,以及一种可能的实践方式。
我们知道领域模型的变化会产生领域事件。例如,用户在完成注册后,系统会发出一封带有确认信息的邮件到用户的邮箱;用户关注的好友发送动态后他会收到相应的通知等等。在业务比较简单或者不用考虑性能的情况下,我们可以直接把对领域事件的处理嵌入到领域服务中。考虑这样一个场景:用户回复了某条评论,那么被回复的那个用户(也就是那条评论的所有者)需要收到一个PUSH消息。这个场景比较简单,我们可能直接写出类似下面的代码:
void reply(long fromUserId,long toUserId,String content) {
saveReply(fromUserId,toUserId,content);
sendPush(toUserId,content);
}
这样一来,我们就直接把发送PUSH的动作嵌入到了回复的逻辑中。这样做有以下两个问题:
开闭原则
。 解决上诉问题的方法很简单,就是使用 领域事件
。 领域事件
很好理解,说白了就是与领域相关的事件。事件的产生往往伴随着相应的动作,例如上面所提到的回复动作。有了领域事件,每个领域本身就只需要关系其自己的业务逻辑,并在处理完自身逻辑的同时抛出相应的领域事件。对这些领域事件感兴趣的业务方可以 订阅
该事件,然后进行后续的处理。这与 观察者
模式和 发布订阅
模式是十分相像的。我更倾向于 发布订阅
这个词,它更好的表达了发布者和订阅者的一种解耦。
发布订阅
模式有很多种的实现,有很多开源框架和类库也实现了这种模式。例如 Spring
中的事件, Guava
中的 EventBus
都是很好的实践。直接采用这些工具会有两个问题:
Spring
框架自带的事件机制是同步的,那么领域事件的发布者的执行流程就和订阅者的处理流程在一个调用堆栈中了,在某些情况下这事不可接收的。 EventBus
是支持同步和异步两种模式的,但是它要求在初始化时就指定好事件是 同步
的还是 异步
的,这对于使用方不够灵活。 Spring
框架让这种订阅关系变得模糊,因为事件的注册是通过事件 ApplicationListener
接口完成的,那么订阅方就无法获得事件发布者的引用,进而无法取消事件的订阅。当然,取消事件订阅的情景并不常见,所以这种情况在大部分场景下也是可以接受的。 无论是出于对事件发送同步异步的控制,还是处于订阅方更高的灵活性要求,自己在这些框架和工具上再进行封装都还是要必要的。下面我给出我的一种实践方案。
我推荐在 guava
的 EventBus
上面进行封装,因为它已经实现了同步和异步的模式,并且使用注解的订阅方式对程序员也十分友好。
首先,我们需要定义一个 领域事件
的抽象基类。
这个抽血基类中定义了发生时间和identify的一个抽象方法,该方法用来标示事件。下面我们就可以定义领域事件的发布器了,如下图所示。
我先定义了领域发布器的一个通用接口,主要包括四个方法:
同时,我给出了一个基于 Guava
的实现,如下:
/**
* Guava事件发布器实现
* Created by Michael Jiang on 16/1/12.
*/
public abstract class GuavaDomainEventPublisher implements DomainEventPublisher {
private EventBus syncBus = new EventBus(identify());
private EventBus asyncBus = new AsyncEventBus(identify(), Executors.newFixedThreadPool(1));
@Override
public void register(Object listener) {
syncBus.register(listener);
asyncBus.register(listener);
}
@Override
public void publish(DomainEvent event) {
syncBus.post(event);
}
@Override
public void asyncPublish(DomainEvent event) {
asyncBus.post(event);
}
}
我在实现中初始化了两个eventBus,一个是同步的 syncBus
,用于发布同步事件;另外一个是异步的 asyncBus
,用于发布异步事件。其中我将异步线程池硬编码为1个线程,基本满足大部分情况,也可酌情修改或者开放这个参数,有各个领域事件的发布器来实现。
具体的领域事件发布器直接继承 GuavaDomainEventPublisher
,并覆盖identify()方法后就可以使用了。
这里我并没有专门去设计订阅方,因为 Guava
提供的注解方式已经十分方便了。我设计了一个简单的demo放倒了github上面,有兴趣的朋友可以直接查看源代码。如果你有更好的设计方法或者思路,可以直接留言进行讨论。
Demo地址: https://github.com/mymonkey110/event-light
This article used CC-BY-SA-3.0 license, please follow it.