领域驱动设计战术模式--领域事件

使用领域事件来捕获发生在领域中的一些事情。

领域驱动实践者发现他们可以通过了解更多发生在问题域中的事件,来更好的理解问题域。这些事件,就是领域事件,主要是与领域专家一起进行知识提炼环节中获得。

领域事件,可以用于一个限界上下文内的领域模型,也可以使用消息队列在限界上下文间进行异步通信。

1 理解领域事件

领域事件是领域专家所关心的发生在领域中的一些事件。

将领域中所发生的活动建模成一系列离散事件。每个事件都用领域对象表示。领域事件是领域模型的组成部分,表示领域中所发生的事情。

领域事件的主要用途:

  • 保证聚合间的数据一致性
  • 替换批量处理
  • 实现事件源模式
  • 进行限界上下文集成

2 实现领域事件

领域事件表示已经发生的某种事实,该事实在发生后便不会改变。因此,领域事件通常建模成值对象。

但,这也有特殊的情况,为了迎合序列化和反序列化框架需求,在建模时,经常会进行一定的妥协。

2.1 创建领域事件
2.1.1 事件命名

在建模领域事件时,我们应该根据限界上下文中的通用语言来命名事件。

如果事件由聚合上的命令操作产生,通常根据该操作方法的名字来命名事件。事件名字表明聚合上的命令方法在执行成功后的事实。即事件命名需要反映过去发生过的事情。

public class AccountEnabledEvent extends AbstractAggregateEvent<Long, Account> {
    public AccountEnabledEvent(Account source) {
        super(source);
    }
}
复制代码
2.1.2 事件属性

事件的属性主要用于驱动后续业务流程。当然,也会拥有一些通用属性。

事件具有一些通用属性,如:

  • 唯一标识
  • occurredOn 发生时间
  • type 事件类型
  • source 事件发生源(只针对由聚合产生的事件)

通用属性可以使用事件接口来规范。

接口或类 含义
DomainEvent 通用领域事件接口
AggregateEvent 由聚合发布的通用领域事件接口
AbstractDomainEvent DomainEvent 实现类,维护 id 和 创建时间
AbstractAggregateEvent AggregateEvent 实现类,继承子 AbstractDomainEvent,并添加 source 属性

但,事件最主要的还是业务属性。我们需要考虑,是谁导致事件的发生,这可能涉及产生事件的聚合或其他参与该操作的聚合,也可能是其他任何类型的操作数据。

2.1.3 事件方法

事件是事实的描述,本身不会有太多的业务操作。

领域事件通常被设计为不变对象,事件所携带的数据已经反映出该事件的来源。事件构造函数完成状态初始化,同时提供属性的 getter 方法。

2.1.4 事件唯一标识

这里需要注意的是事件唯一标识,通常情况下,事件是不可变的,那为什么会涉及唯一标识的概念呢?

对于从聚合中发布出来的领域事件,使用事件的名称、产生事件的标识、事件发生的时间等足以对不同的事件进行区分。但,这样会增加事件比较的复杂性。

对于由调用方发布的事件,我们将领域事件建模成聚合,可以直接使用聚合的唯一标识作为事件的标识。

事件唯一标识的引入,会大大减少事件比较的复杂性。但,其最大的意义在于限界上下文的集成。

当我们需要将领域事件发布到外部的限界上下文时,唯一标识就是一种必然。为了保证事件投递的幂等性,在发送端,我们可能会进行多次发送尝试,直至明确发送成功为止;而在接收端,当接收到事件后,需要对事件进行重复性检测,以保障事件处理的幂等性。此时,事件的唯一标识便可以作为事件去重的依据。

事件唯一标识,本身对领域建模影响不大,但对技术处理好处巨大。因此,将它作为通用属性进行管理。

2.2 发布领域事件

我们如何避免领域事件与处理者间的耦合呢?

一种简单高效的方式便是使用观察者模式,这种模式可以在领域事件和外部组件间进行解耦。

2.2.1 发布订阅模型

为了统一,我们需要定义了一套接口和实现类,以基于观察者模式,完成事件的发布。

涉及接口和实现类如下:

接口或类 含义
DomainEventPublisher 用于发布领域事件
DomainEventHandlerRegistry 用于注册 DomainEventHandler
DomainEventBus 扩展自 DomainEventPublisher 和 DomainEventHandlerRegistry 用于发布和管理领域事件处理器
DefaultDomainEventBus DomainEventBus 默认实现
DomainEventHandler 用于处理领域事件
DomainEventSubscriber 用于判断是否接受领域事件
DomainEventExecutor 用于执行领域事件处理器

使用实例如 DomainEventBusTest 所示:

public class DomainEventBusTest {
    private DomainEventBus domainEventBus;

    @Before
    public void setUp() throws Exception {
        this.domainEventBus = new DefaultDomainEventBus();
    }

    @After
    public void tearDown() throws Exception {
        this.domainEventBus = null;
    }

    @Test
    public void publishTest(){
        // 创建事件处理器
        TestEventHandler eventHandler = new TestEventHandler();
        // 注册事件处理器
        this.domainEventBus.register(TestEvent.class, eventHandler);

        // 发布事件
        this.domainEventBus.publish(new TestEvent("123"));

        // 检测事件处理器是够运行
        Assert.assertEquals("123", eventHandler.data);
    }

    @Value
    class TestEvent extends AbstractDomainEvent{
        private String data;
    }

    class TestEventHandler implements DomainEventHandler<TestEvent>{
        private String data;
        @Override
        public void handle(TestEvent event) {
            this.data = event.getData();
        }
    }
}
复制代码

在构建完发布订阅结构后,需要将其与领域模型进行关联。领域模型如何获取 Publisher,事件处理器如何进行订阅。

2.2.2 基于 ThreadLocal 的事件发布

比较常用的方案便是将 DomainEventBus 绑定到线程上下文。这样,只要是同一调用线程都可以方便的获取 DomainEventBus 对象。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值