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

选中文字可对指定文章内容进行评论啦,→和←可快速切换按钮,绿色背景文字可以点击查看评论额。

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

 

 

 

 

 

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

相关推荐

JavaScript:给动态元素绑定事件

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

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

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

如何修改Git已提交的日志

在某些时候,你发现了之前提交到git上的日志描述不全或者描述有误,这时你是会想要修改它的。 但提交已经是push到服务器,甚至是已经有好几个提交在后面了,这个怎么办呢?Git提供了一些方法来修改。下面分为四种情况来处理。情况一:最后一次提交且未push执行以下命令:git commit --amend git会打开$EDITOR编辑器,它会加载这次提交的日志,这样我们

如何对REST API进行版本控制

如果您对API不太熟悉,您可能会想...为什么对API版本控制大惊小怪?如果您对API的更改感到厌倦,那么您可能会大惊小怪。如果您是API的维护者,那么您可能还会大惊小怪地尝试解决诸如此类的难题:# 下面的v2(版本2)表示的只是产品版本还是整个APId呢?/v2/products# 是什么促使v1和v2的更改呢? 它们之间有什么不同呢?/v1/products/v2/products这些有关版本

CKEditor5事件系统(事件优先级)

今天继续学习CK5的事件系统,上一节我们知道了绑定和取消绑定事件的两种方法,知道在一个emitter上一个同名事件可以绑定多个回调函数,自然问题来了,这些函数的执行顺序是怎么样的呢?CK5的事件监听优先级实际上,对于一个同名事件,CK5提供了事件优先级功能,如下代码所示const anyClass = new AnyClass(); anyClass.on( 'eventName', ( even

CKEditor5事件系统(代理事件)

emitter接口提供了事件代理机制。也就是说指定选择的事件能够被其他的emitter触发。 1、代理指定的事件到另一个emitterlet anyClass = new AnyClass(); let anotherClass = new AnyClass(); let oneClass = new AnyClass(); anotherClass.on('bar',(evt,data

javascript——嵌套函数作用域

javascript作用域我们知道,js中有三个作用域,分别是block scope(块作用域),function scope(函数作用域),globle scope(全局作用域);今天我们来看看什么是嵌套函数作用域嵌套函数作用域let a = 10; function outer() { let b = 20; function inner() { let c = 30; conso