领域驱动设计——如何发布领域事件

时间:2021-08-26 19:02:37

领域事件(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. 领域事件是在聚合方法处理过程中立即发布,聚合方法的事务可能还没有完成提交,其他事件监听的处理器便可以开始执行。如果事件处理器和聚合方法需要有顺序执行,这会导致执行乱序的副作用。
  2. 违法单一职责的原则,领域聚合除了要处理聚合自身业务外,还需要考虑事件的发布。这样导致聚合业务和事件发布耦合。

二、领域聚合方法返回领域事件,在应用服务中发布

在领域聚合方法执行过程中,把发生的领域事件存放在集合中,聚合方法执行完成后,返回领域事件集合。返回的领域事件集合交由应用服务决定,什么时候发布以及如何发布领域事件。

如在《微服务架构设计模式》示例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.

大意是分为两种情况考虑:

  1. 返回的结果用于程序的其他地方做决策,则不违反Tell Don't Ask原则。
  2. 如果返回的结果用于调用原对象方法做决策,则违反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

思考:

  1. 应用服务或者其他组件需要能够访问聚合内的事件集合,这是否违反封装
  2. 因为发布事件的组件是管理多个聚合的事件集合,如果聚合方法为执行完,组件就获取事件集合发布,是否也存在乱序的问题呢?

这个方法未看到很多例子,不展开讨论。

参考:

http://www.kamilgrzybek.com/design/how-to-publish-and-handle-domain-events/