实现领域事件

当你的系统或者业务变得日益复杂时,DDD的模式是一种非常值得尝试的架构模式。DDD让你更加关注于你的业务领域,思考你的业务模型,帮组你理清繁杂的业务关系。我推荐所有还没有了解过或者接触过DDD的后端工程师都去学习一下该架构模式。本文主要关注DDD中的领域事件,以及一种可能的实践方式。

我们知道领域模型的变化会产生领域事件。例如,用户在完成注册后,系统会发出一封带有确认信息的邮件到用户的邮箱;用户关注的好友发送动态后他会收到相应的通知等等。在业务比较简单或者不用考虑性能的情况下,我们可以直接把对领域事件的处理嵌入到领域服务中。考虑这样一个场景:用户回复了某条评论,那么被回复的那个用户(也就是那条评论的所有者)需要收到一个PUSH消息。这个场景比较简单,我们可能直接写出类似下面的代码:

1
2
3
4
  void  reply(long fromUserId,long toUserId,String content) {
saveReply(fromUserId,toUserId,content);
sendPush(toUserId,content);
}

这样一来,我们就直接把发送PUSH的动作嵌入到了回复的逻辑中。这样做有以下两个问题:

  • 回复动作处理了它不关心的逻辑。发送PUSH不是回复的强关联逻辑,也就是说即使push发送不成功也应该让回复动作成功。上面的代码将回复和发送PUSH耦合在了一起。
  • 如果出现了多个对回复动作感兴趣的业务方,那么上面的代码将不可维护。比如,我们有一个回复的计数器,它要统计回复的总量。如果把增加计数器的动作写在回复中,那么将不可维护,因为每次出现新的业务方都要修改回复逻辑。这显然返回了开闭原则

解决上诉问题的方法很简单,就是使用领域事件领域事件很好理解,说白了就是与领域相关的事件。事件的产生往往伴随着相应的动作,例如上面所提到的回复动作。有了领域事件,每个领域本身就只需要关系其自己的业务逻辑,并在处理完自身逻辑的同时抛出相应的领域事件。对这些领域事件感兴趣的业务方可以订阅该事件,然后进行后续的处理。这与观察者模式和发布订阅模式是十分相像的。我更倾向于发布订阅这个词,它更好的表达了发布者和订阅者的一种解耦。

发布订阅模式有很多种的实现,有很多开源框架和类库也实现了这种模式。例如Spring中的事件,Guava中的EventBus都是很好的实践。直接采用这些工具会有两个问题:

  • 无法灵活的处理同步事件和异步事件。Spring框架自带的事件机制是同步的,那么领域事件的发布者的执行流程就和订阅者的处理流程在一个调用堆栈中了,在某些情况下这事不可接收的。EventBus是支持同步和异步两种模式的,但是它要求在初始化时就指定好事件是同步的还是异步的,这对于使用方不够灵活。
  • 订阅方无法控制事件的订阅与取消。出于解耦和灵活性的考虑,我们往往把事件注册的动作放倒订阅方。Spring框架让这种订阅关系变得模糊,因为事件的注册是通过事件ApplicationListener接口完成的,那么订阅方就无法获得事件发布者的引用,进而无法取消事件的订阅。当然,取消事件订阅的情景并不常见,所以这种情况在大部分场景下也是可以接受的。

无论是出于对事件发送同步异步的控制,还是处于订阅方更高的灵活性要求,自己在这些框架和工具上再进行封装都还是要必要的。下面我给出我的一种实践方案。

我推荐在guavaEventBus上面进行封装,因为它已经实现了同步和异步的模式,并且使用注解的订阅方式对程序员也十分友好。

首先,我们需要定义一个领域事件的抽象基类(DomainEvent)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 领域事件基类
* Created by Michael Jiang on 16/1/12.
*/
public abstract class DomainEvent {
private Date occurredTime;

protected abstract String identify();

public DomainEvent() {
occurredTime =new Date();
}

public Date getOccurredTime() {
return occurredTime;
}
}

这个抽血基类中定义了发生时间和identify的一个抽象方法,该方法用来标示事件。下面我们就可以定义领域事件的发布器了(EventPublisher),如下图所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 领域事件发射器
* Created by Michael Jiang on 16/1/12.
*/
public interface DomainEventPublisher<T extends DomainEvent> {
String identify();

void register(Object listener);

void publish(T event);

void asyncPublish(T event);
}

我先定义了领域发布器的一个通用接口,主要包括四个方法:

  • identify() 发布器标示,用来区分不同的发布器。
  • register(Object) 注册接口,订阅方调用该接口来订阅事件。
  • publish(T event) 同步发布事件接口
  • asyncPublish(T event) 异步发布事件接口

同时,我给出了一个基于Guava的实现,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 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

坚持原创技术分享,您的支持将鼓励我继续创作!