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

领域事件(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/

 

 

 

 

 

版权声明:著作权归作者所有。

相关推荐

给本地localhost域名添加https证书

本文介绍如何给本地域名localhost添加证书,但此方法仅限在开发环境使用。在生产环境中,强烈禁止使用自签名证书。创建认证中心(Certificate authority,CA)生成RootCA.pem, RootCA.key 以及 RootCA.crt:openssl req -x509 -nodes -new -sha256 -days 1024 -newkey rsa:2048 -keyo

如何正确实现Android启动屏画面(避免白屏)

Android启动屏不正确的实现可能会导致用户长时间等待,或者可能会出现黑白屏。这里简单演示如何正确实现Android启动屏。演示分为以下几个步骤:在res/drawable文件夹中创建splash_background.xml文件。编辑res/values/styles.xml创建java/.../SplashActivity编辑manifests/AndroidManifest.xml1、在r

Nginx同一个域名配置多个项目

使用Nginx要在同一个域名下配置多个项目有两种方式:nginx按不同的目录分发给不同的项目启用二级域名,不同的项目分配不同的二级域名nginx按不同的目录分发给不同的项目在nginx的server节点配置server {     listen    80;     server_name example.c

JavaScript:给动态元素绑定事件

JavaScript给动态添加的元素绑定事件有几种方式:方法一:jQuery使用jQuery.fn.on可以很简单为动态元素绑定事件:$(staticAncestors).on(eventName, dynamicChild, function() {}); staticAncestors:静态的祖先元素选择器eventName:事件名,如click等dynamicC