回顾计算机硬件的工作原理,不难发现计算机的整个工作过程其实就是一个事件处理的过程。当你点击鼠标、敲击键盘或插入U盘时,计算机会以中断的形式处理各种外部事件。在软件开发领域,事件驱动架构(EDA)早已被开发人员用于各种实践。典型的应用场景包括浏览器处理用户输入、消息机制、SOA。近年来重新进入开发者视野的ReactiveProgramming将事件视为编程模型中的一等公民。可见,“事件”的概念在计算机科学领域一直扮演着重要的角色。理解领域事件领域事件(DomainEvents)是领域驱动设计(DDD)中的一个概念,用来捕捉我们建模的领域中发生的事情。领域事件本身作为通用语言的一部分,成为所有项目成员(包括领域专家)的交流语言。比如在用户注册的过程中,我们可能会说“当用户注册成功后,向客户发送欢迎邮件”,此时“用户已注册”就是一个域事件。当然,并非所有发生的事情都可以是领域事件。一个领域事件必须对业务有价值,并有助于形成一个完整的业务闭环,即一个领域事件将导致进一步的业务运作。举个咖啡店建模的例子,当有顾客来到前台时,会产生“顾客已到”的事件。如果你关注客户接待,比如需要为客户预留位置,那么此时“客户已到”是一个典型的领域事件,因为它会被用来触发下一步——“预留位置”一个地方”动作;但是如果你正在为一个咖啡结账系统建模,那么“顾客已经到了”在这一点上并没有太多必要——你不能在用户一到就向顾客要钱,是的,“客户已下订单”对于结账系统来说是一个有用的事件。在微服务(Microservices)架构的实践中,人们借鉴了DDD中的很多概念和技术。比如一个微服务应该对应DDD中的一个限界上下文(BoundedContext);聚合根;以及微服务之间集成时DDD中的反腐层(Anti-CorruptionLayer,ACL);甚至可以说DDD和微服务有着天然的默契。关于DDD的更多信息,请参考作者的另一篇文章或参考《领域驱动设计》和《实现领域驱动设计》。DDD中有一个原则:一个业务用例对应一个事务,一个事务对应一个聚合根,即在一个事务中,只能操作一个聚合根。但是在实际应用中,我们经常会发现一个用例需要修改多个聚合根,而不同的聚合根仍然处于不同的限界上下文中。比如你在电商网站上买东西,你的积分就会相应增加。这里的购买行为可以建模为Order对象,积分可以建模为Account对象的一个??属性。订单和账户都是聚合根,分别属于订单系统和账户系统。显然,我们需要保持订单和积分之间的数据一致性。通常的做法是在同一个事务中更新两者,但是这样会存在以下问题:违背了DDD中“单个事务修改单个聚合根”的设计原则;不同系统之间需要采用重量级的分布式事务(DistributedTransactions,又称XA事务或全局事务);在不同系统之间产生强耦合。通过引入领域事件,我们可以很好地解决上述问题。总的来说,领域事件给我们带来了以下好处:解耦微服务(限界上下文);帮助我们深入理解领域模型;为审计和报告提供数据源;往EventSourcing和CQRS等方向发展。还是以上面的电商网站为例。用户下单后,订单系统会发送“用户已下单”的字段事件发布到消息系统。这个时候,订单就完成了。账户系统在消息系统中订阅“用户已下单”事件,当事件到达时进行处理,提取事件中的订单信息,然后调用自己的积分引擎(或其他微服务)计算积分,***更新用户积分。可以看到,此时订单系统发送完事件后,整个用例操作就结束了,不需要关心谁收到了事件,也不需要关心事件做了什么处理。事件的消费者可以是账户系统,也可以是任何对事件感兴趣的第三方,比如物流系统。从而解开了各个微服务之间的耦合关系。值得注意的是,此时微服务之间并没有强一致性,而是基于事件的最终一致性。事件风暴(EventStorming)是一种团队活动,旨在通过领域事件识别聚合根,进而划分微服务的有界上下文。活动中,团队首先通过头脑风暴列出领域内的所有领域事件,然后将它们整合形成最终的领域事件集合。然后,对于每一个事件,它都标记了引起该事件的命令(Command),然后对于每一个事件标记了命令发起者的角色。该命令可以由用户发起,也可以由第三方系统调用或由定时器触发。***对事件进行分类以整理聚合根和有界上下文。事件风暴的另一个好处是可以加深相关人员的领域知识。应该注意的是,在事件风暴活动期间,领域专家必须在场。有关事件风暴的更多信息,请参阅此处。创建领域事件领域事件应该回答诸如“谁在什么时候做了什么”这样的问题。在实际编码中,可以认为层超类型(LayerSupertype)包含一些事件的公共属性:}可以看出,领域事件还包含了ID,但ID并不是实体(Entity)层面的ID概念。相反,它主要用于事件跟踪和日志记录。此外,由于领域事件描述了过去发生的事情,我们应该将领域事件建模为不可变的。从DDD的概念来看,领域事件更像是一个特殊的值对象(ValueObject)。对于上面提到的咖啡店示例,创建“客户已到达”事件如下:Event除了活动的属性外,还自定义了一个与活动密切相关的业务属性——客户数量(customerNumber)——以便后续操作可以预留相应数量的席位。另外,我们将所有的属性和CustomerArrivedEvent本身声明为final,不暴露任何可能修改这些属性的方法,从而保证了事件的不变性。发布领域事件在使用领域事件时,我们通常使用“发布-订阅”的方式来集成不同的模块或系统。在单个微服务中,我们可以使用领域事件来集成不同的功能组件。例如上面提到的“用户注册后向用户发送欢迎邮件”的例子,注册组件发出一个事件,邮件发送组件收到这个事件后向用户发送一封邮件。在微服务内部使用领域事件时,我们不一定要引入消息中间件(如ActiveMQ等)。仍以上述“注册后发送欢迎邮件”为例。虽然注册行为和发送邮件行为通过领域事件整合在一起,但它们仍然发生在同一个线程中,并且是同步的。另外需要注意的是,在限界上下文中使用领域事件时,我们仍然需要遵循“一个事务只更新一个聚合根”的原则。违反这一点通常意味着我们对聚合根的拆分是错误的。即使确实存在这样的情况,也应该以异步的方式针对不同的聚合根使用不同的事务(此时需要引入消息中间件),此时可以考虑后台任务。领域事件除了用在微服务内部,更多时候是用来整合不同的微服务,比如上面的“电商订单”例子。通常,领域事件源自领域对象,或者更准确地说,源自聚合根。在具体的编码实现中发布领域事件的方式有很多种。一种直接的方式是直接调用聚合根中发布事件的Service对象。以上面的“电商订单”为例,在创建订单时,会发布领域事件“ordercreated”。此时可以考虑在订单对象的构造函数中发布事件:publicclassOrder{publicOrder(EventPublishereventPublisher){//createorder//...eventPublisher.publish(newOrderPlacedEvent());}}(注:为了重点在事件发布上,我们简化了Order对象,Order对象本身在实际编码中是没有引用的。)可见,为了发布OrderPlacedEvent事件,我们需要传入Service对象EventPublisher,这显然是一种API污染,即OrderasAdomainobject只需要关注与业务相关的数据,而不是EventPublisher等基础设施对象。NServiceBus的创始人UdiDahan提出了另一种方法,就是通过调用领域对象中EventPublisher上的静态方法来发布领域事件:publicclassOrder{publicOrder(){//createorder//...EventPublisher.publish(newOrderPlacedEvent());}}这种方法虽然避免了API污染,但是这里的publish()静态方法会产生副作用,给Order对象的测试带来困难。此时,我们可以通过“在聚合根中临时保存领域事件”来进行改进:}publicList
