领域事件(Domain Event)是域驱动设计的构建块之一,它通常是一个以过去时命名的不可变数据容器类。
如:
public class OrderPlaced
{
private Order order;
public OrderPlaced(Order order){
this.order = order;
}
public Order getOrder() {
this.Order;
}
}
发布领域事件的三种方式
主要有三种方式发布领域事件。
一、在领域模型内发布领域事件
在领域模型内领域事件通常的做法是,在领域模型内使用静态方法引用事件发布器,如DomainEventPublisher,在领域事件发生的地方立即发布事件。这里需要强调的是立即发布,因为所有领域事件Handler也立即开始处理(甚至聚合方法没有完成处理)。
如在《实现领域驱动设计》里的示例:
public void initiateDiscussion(DiscussionDescriptor aDescriptor) {
if (aDescriptor == null) {
throw new IllegalArgumentException("The descriptor must not be null.");
}
if (this.discussion().availability().isRequested()) {
this.setDiscussion(this.discussion().nowReady(aDescriptor));
DomainEventPublisher
.instance()
.publish(new ProductDiscussionInitiated(
this.tenantId(),
this.productId(),
this.discussion()));
}
}
示例中,当讨论可以用于请求时,立即发送产品讨论已初始化事件ProductDiscussionInitiated
。
这种方式我认为有两个缺点:
- 领域事件是在聚合方法处理过程中立即发布,聚合方法的事务可能还没有完成提交,其他事件监听的处理器便可以开始执行。如果事件处理器和聚合方法需要有顺序执行,这会导致执行乱序的副作用。
- 违法单一职责的原则,领域聚合除了要处理聚合自身业务外,还需要考虑事件的发布。这样导致聚合业务和事件发布耦合。
二、领域聚合方法返回领域事件,在应用服务中发布
在领域聚合方法执行过程中,把发生的领域事件存放在集合中,聚合方法执行完成后,返回领域事件集合。返回的领域事件集合交由应用服务决定,什么时候发布以及如何发布领域事件。
如在《微服务架构设计模式》示例1:
聚合:Ticket.java
public List<TicketDomainEvent> confirmCreate() {
switch (state) {
case CREATE_PENDING:
state = TicketState.AWAITING_ACCEPTANCE;
return singletonList(new TicketCreatedEvent(id, new TicketDetails()));
default:
throw new UnsupportedStateTransitionException(state);
}
}
在应用服务发布事件:KitchenService
public void confirmCreateTicket(Long ticketId) {
Ticket ro = ticketRepository.findById(ticketId)
.orElseThrow(() -> new TicketNotFoundException(ticketId));
List<TicketDomainEvent> events = ro.confirmCreate();
domainEventPublisher.publish(ro, events);
}
在Ticket聚合中返回的事件列表,然后再KitchenService的应用服务中,调用DomainEventPublisher发布事件。
这种方式避免了领域聚合内发布领域事件上述的两个缺点。
领域聚合方法返回领域事件是否违反CQS的争议
但这种方式也有争议:它是否违反了命令查询分离(CQS,Command-Query-Separation)原则呢?或者是Tell,Don't Ask原则。
参考:How to choose between Tell don't Ask and Command Query Separation?里的一个回答(此回答也是有争议的):
If you are using the result of a method call to make decisions elsewhere in the program, then you are not violating Tell Don’t Ask. If, on the other hand, you’re making decisions for an object based on a method call to that object, then you should move those decisions into the object itself to preserve encapsulation.
大意是分为两种情况考虑:
- 返回的结果用于程序的其他地方做决策,则不违反Tell Don't Ask原则。
- 如果返回的结果用于调用原对象方法做决策,则违反Tell Don't Ask原则。这种情况建议把决策代码移到对象内部,以提高对象的封装。
参考:Don't publish Domain Events, return them!,针对聚合方法返回领域事件的做法,作者也是以上面的回答做依据。认为领域事件是应用的功能,而非领域聚合的业务。如果只返回领域事件给应用服务做决策则不违反CQS。作者也提了如果返回除了领域事件还包含了领域聚合的可变状态,则违反了CQS。
《微服务架构设计模式》示例2:
public Ticket createTicket(long restaurantId, Long ticketId, TicketDetails ticketDetails) {
ResultWithDomainEvents<Ticket, TicketDomainEvent> rwe = Ticket.create(restaurantId, ticketId, ticketDetails);
ticketRepository.save(rwe.result);
domainEventPublisher.publish(rwe.result, rwe.events);
return rwe.result;
}
这个示例中,ResultWithDomainEvents
除了返回领域事件外,还包含了Ticket聚合,这种做法是违反了CQS的。
三、聚合内置事件集合,聚合外部获取事件集合发布
这种方式是在每个聚合内创建一个领域事件集合。在聚合方法执行过程中,每个域事件实例都会添加到此集合。执行后,ApplicationService(或其他组件)从所有聚合实体内读取所有事件集合并发布它们。
详情参考:A better domain events pattern。
思考:
- 应用服务或者其他组件需要能够访问聚合内的事件集合,这是否违反封装
- 因为发布事件的组件是管理多个聚合的事件集合,如果聚合方法为执行完,组件就获取事件集合发布,是否也存在乱序的问题呢?
这个方法未看到很多例子,不展开讨论。
参考:
http://www.kamilgrzybek.com/design/how-to-publish-and-handle-domain-events/