命令模型

Command Model

在一个基于CQRS的应用程序中,一个领域模型(如Eric Evans和Martin Fowler所定义的)可以是一个非常强大的机制,这种机制可以处理聚合状态发生改变时的校验和执行时所带来的复杂性。通常一个典型的领域模型会有大量的构建块,但是在CQRS中的Command 处理过程中有一个重要的角色就是:聚合。

Command是应用程序状态发生改变的源头。命令是表达意图(它描述你想要做什么)和你所须要的信息的组合。命令模型用于处理传入的命令,以验证它并定义他的输出结果。在这个模型中,一个Command Handler负责处理某种类型的命令,并根据其中包含的信息做出相应的逻辑处理。

Aggregate聚合

聚合是始终保持一致状态的一个实体或一组实体。聚合根是负责维护该聚合内一致状态的聚合树顶部的对象。这使得Aggregate成为在任何基于CQRS的应用程序中实现命令模型的主要构建块。

注意:

术语“聚合”是指Evans在领域驱动设计中定义的聚合:

因为数据状态发生改变所引起的相关联的对象被视为一个单元(聚合),在外部只能引用聚合的聚合根对象。在聚合内部使用一套一致性规则。

例如,“Contact”聚合包含了两个实体:Contact和Address。为了使整个聚合保持一致的状态,应该通过Contact实体来添加地址给联系人。在这种情况下,Contact实体是聚合根。

在Axon中,聚合由一个聚合标识来标识。他可以是任何对象,但是标识符的最好的做法是这样的:

1.实现equals和hashCode方法来保证与其他实例进行相等比较

2.实现一个提供一致结果的的toString()方法(标识符相等的话,那么toString()方法的结果也应该相等.
3.标识符是可序列化的

当聚合使用不兼容的识符时,测试程序(见测试)将验证这些条件并使测试失败。String,UUID和数字类型的标识符是最好的。不要使用原始类型(比如int这些)作为标识符,因为它们不允许延迟初始化。在某些情况下,Axon可能会错误地假设原始类型的默认值是标识符的值。

注意:一个好习惯就是使用随机生成的标识符,而不是按顺序生成标识符。使用顺序生成的序列会大大降低应用程序的可伸缩性,因为机器需要保持彼此最后一次使用的最新的序列号。与UUID冲突的机会非常渺茫(如果生成8.2×10 11个UUID,则机会为10的-15次方)。

聚合的实现

聚合的操作总是交给一个称为聚集根的实体来控制。通常,这个实体的名称和聚合名完全一样。例如,Order聚合可能由Order实体引用几个Orderline实体组合而成.Order和Orderline一起形成聚合。虽然根据CQRS原则来讲这并不完全正确,但他也可以通过方法来暴露出聚合的状态。

聚合根必须声明包含聚合标识符的字段。这个标识符必须在第一个事件发布的时候最初被初始化。该标识符字段必须由@AggregateIdentifier注解注释。如果您使用JPA并在聚合上使用JPA注解,则Axon也可以使用JPA提供的@Id注解。

聚合可以使用AggregateLifecycle.apply()方法来注册要发布的事件。与EventBus不同的是,消息需要被包装在EventMessage中,apply()允许你直接传递payload对象。

import static org.axonframework.commandhandling.model.AggregateLifecycle.apply;

@Entity // Mark this aggregate as a JPA Entity
public class MyAggregate {

@Id // When annotating with JPA @Id, the @AggregateIdentifier annotation is not necessary
private String id;

// fields containing state...

@CommandHandler
public MyAggregate(CreateMyAggregateCommand command) {
    // ... update state
    apply(new MyAggregateCreatedEvent(...));
}

// constructor needed by JPA
protected MyAggregate() {
}

}

通过定义一个带@EventHandler注解的方法,聚合内的实体能监听聚合发布的事件。当一个EventMessage发布时这些方法将被调用(在任何外部处理器发布之前)。

聚合的事件溯源

除了存储Aggregate的当前状态之外,还可以根据它过去发布的Events来重建Aggregate的状态。为了要达到这样的效果,所有的状态改变必须由一个事件来表示。

事件源聚合类似于一种被约定成俗的聚合:它们必须声明一个标识符,并可以使用apply方法来发布事件。然而,事件溯源聚合类的状态更改(即字段值的任何更改)必须在被@EventSourcingHandler注解的方法上执行。这包括了设置聚合根的标识符。

我们须要注意的是,聚合标识符必须在聚合发布的第一个事件中就添加进去,以便在@EventSourcingHandler注解的方法将聚合的标识符进行赋值(注:也就是@EventSourcingHandler中处理的event的聚合标识符不能为空)。他们通常是在创建事件的时候就把聚合标识符添加到事件中。

事件源聚合类的聚合根必须包含一个无参数构造函数。Axon框架会用这个构造方法创建一个空的Aggregate实例,他会在事件溯源的时候调用。加载聚合时,如果找不到这个无参构造函数将导致异常。

publicclass MyAggregateRoot {

@AggregateIdentifier

private String aggregateIdentifier;

// fields containing state...

@CommandHandler

public MyAggregateRoot(CreateMyAggregate cmd) {

    apply(new MyAggregateCreatedEvent(cmd.getId()));

}

// constructor needed for reconstruction

protected MyAggregateRoot() {

}

@EventSourcingHandler

private void handleMyAggregateCreatedEvent(MyAggregateCreatedEvent event) {

// make sure identifier is always initialized properly

this.aggregateIdentifier = event.getMyAggregateIdentifier();

// ... update state

}

}

带@EventSourcingHandler注解的方法会使用特定的规则来解析。这些规则同样对带有@EventHandler注解的方法也一样适合,他们在Defining Event Handlers这一章节中有详细的解释。

注意:Event handler方法可以是私有的,只要JVM的安全设置允许Axon框架更改方法的可访问性即可。这样可以清楚地将聚合的公共API(公开生成事件的方法)从处理事件的内部逻辑中分离出来。大多数的IDE有一个选项用来忽略“未使用的私有方法”的警告方法。或者,您可以向该方法添加@SuppressWarnings(“UnusedDeclaration”)注解,以确保不会出现一不小心就将Event handler方法删除。

在有些情况下,特别是当聚合有很多实体的时候,对同一聚合的其他实体中事件发布的影响更明显。然而,由于在聚合进行事件溯源的时候也会调用 Event Handler方法,因此必须采取特别的预防措施。

我们可以在Event Sourcing Handler方法内通apply()发布一个新事件。这使得实体 B可以发布一个事件来响应实体A。当进行事件重演的时候,Axon会忽略这个apply()方法而不会去调用他。请注意,在这种情况下,在所有实体接收到第一个事件后,内部apply()调用的事件只会发布给实体。如果有更多的事件须要发布,可以使用apply(...).andThenApply(...)这种方法来实现。

您也可以使用静态的AggregateLifecycle.isLive()方法来检查聚合是否是“live”。如果一个聚合能够完成对历史事件的重演,那么他就是live的。在重演这些事件时,isLive()将返回false。

复杂的聚合结构

一个只包含聚合根的聚合来处理很复杂的业务逻辑往往是不现实的。在这种情况下,我们须要将这种复杂性分散到聚合的各个实体上。当使用事件溯源时,不仅聚合根需要使用事件来触发状态转换,而且聚合内其他实体也如此。

注意:对聚合不应该暴露状态的规则的普片误解是所有实体都不应该包含任何属性访问方法。 这并非如此。事实上,在同一聚合内的实体向其他的实体暴露状态,可能会使一个聚合受益很多。然而,建议不要向外部暴露聚合的状态。

Axon为复杂的聚合结构提供了对事件溯源的支持。实体就像聚合根,简单的对象。声明子实体的字段必须用@aggregateMember注解。这个注解告诉Axon被注解的字段,包含一个应该对Command 和 Event Handlers进行检查的类。

当一个实体(包括聚合根)发布一个事件,这个事件首先应该被聚合根处理。然后通过@AggregateMember注解的作用传到其子实体。

包含子实体的字段必须用@AggregateMember注解,此注释可用于多种字段类型:

l 实体类型,在字段中直接引用;

l 内部包含一个Iterable字段(包括所有集合,如Set,List等)

l 内部包含java.util.Map字段的值

聚合中的命令处理

推荐直接在包含处理状态命令的Aggregate中定义Command Handlers,因为命令处理程序可能需要该Aggregate的状态来完成其工作。

在Aggregate中定义Command Handlers只须要在你要用的方法上面添加一个@CommandHandler注解就行了。带@CommandHandler注解方法的处理规则和其他处理方法都是一样的。但是,Command不仅仅是通过它们的payload进行路由。Command Messages(命令消息)会携带一个名称,该名称默认为Command对象的完整的类名称。

默认情况下,带@CommandHandler注解的方法允许以下参数类型:

l 第一个参数是Command Message中的payload.如果@CommandHandler显示的定义了他要处理的Command的名字,那么他的类型可以是Message,或者CommandMessage.默认情况下一个Command的名字是这个Command的payload的完整的类名称。

l 用@MetaDataValue注解的参数,将用注解上的键对元数据值进行解析。如果这个值是false(默认),则当元数据值不存在时会传递null。如果值是true,而元数据值不存时,这时解析器会发现错误,并阻止该方法的调用。

l 参数为MetaData的话,那么将注入一个CommandMessage的整个MetaData。

l 如果参数是UnitOfWork的话,那么将获取当前的UnitOfWork并注入进来。

l 如果参数Message, or CommandMessage的话,他们会获取整个的数据,它们包括Meta Data元数据和payload.如果一个方法需要多个元数据字段,或者包装消息的其他属性,这很有用处。

你可能感兴趣的:(命令模型)