繁体   English   中英

CQRS 中命令处理程序、聚合、存储库和事件存储之间的关系

[英]Relation between command handlers, aggregates, the repository and the event store in CQRS

我想了解基于 CQRS 的系统中命令处理程序、聚合、存储库和事件存储之间关系的一些细节。

到目前为止我所理解的:

  • 命令处理程序从总线接收命令。 他们负责从存储库加载适当的聚合并调用聚合上的域逻辑。 完成后,他们从总线上删除命令。
  • 聚合提供行为和内部状态。 状态从不公开。 改变状态的唯一方法是使用行为。 对此行为建模的方法从命令的属性创建事件,并将这些事件应用于聚合,聚合又调用相应地设置内部状态的事件处理程序。
  • 存储库仅允许在给定 ID 上加载聚合,并添加新聚合。 基本上,存储库将域连接到事件存储。
  • 最后但并非最不重要的一点是,事件存储负责将事件存储到数据库(或任何使用的存储),并将这些事件重新加载为所谓的事件流。

到现在为止还挺好。 现在有一些我还没有得到的问题:

  • 如果命令处理程序要在现有聚合上调用行为,那么一切都非常简单。 命令处理程序获取对存储库的引用,调用其 loadById 方法并返回聚合。 但是当还没有聚合但应该创建一个聚合时,命令处理程序会做什么? 根据我的理解,稍后应该使用事件重建聚合。 这意味着聚合的创建是为了响应 fooCreated 事件而完成的。 但是为了能够存储任何事件(包括 fooCreated 事件),我需要一个聚合。 所以这在我看来就像一个先有鸡还是先有蛋的问题:没有事件我无法创建聚合,但唯一应该创建事件的组件是聚合。 所以基本上归结为:我如何创建新的聚合,谁做什么?
  • 当聚合触发事件时,内部事件处理程序对其做出响应(通常通过应用方法调用)并更改聚合的状态。 这个事件是如何交给存储库的? 谁发起了“请将新事件发送到存储库/事件存储”操作? 聚合本身? 通过观察聚合存储库? 订阅内部事件的其他人? ……?
  • 最后但并非最不重要的是,我在正确理解事件流的概念时遇到了问题:在我的想象中,它只是类似于事件的有序列表。 重要的是它是“有序的”。 这是正确的吗?

以下内容基于我自己的经验以及我对 Lokad.CQRS、NCQRS 等各种框架的实验。我相信有多种方法可以解决这个问题。 我会发布对我来说最有意义的内容。

1. 聚合创建:

每次命令处理程序需要聚合时,它都会使用存储库。 存储库从事件存储中检索相应的事件列表并调用重载的构造函数,注入事件

var stream = eventStore.LoadStream(id)
var User = new User(stream)

如果聚合之前不存在,则流将为空,新创建的对象将处于其原始状态。 您可能希望确保在此状态下只允许使用少数命令使聚合User.Create() ,例如User.Create()

2. 新事件的存储

命令处理发生在工作单元内 在命令执行期间,每个结果事件都将添加到聚合( User.Changes )内的列表中。 执行完成后,更改将附加到事件存储中。 在下面的示例中,这发生在以下行中:

store.AppendToStream(cmd.UserId, stream.Version, user.Changes)

3. 事件顺序

想象一下,如果两个后续CustomerMoved事件以错误的顺序重播会发生什么。

一个例子

我将尝试用一段伪代码来说明(我故意将存储库问题留在命令处理程序中以显示幕后会发生什么):

申请服务:

UserCommandHandler
    Handle(CreateUser cmd)
        stream = store.LoadStream(cmd.UserId)
        user = new User(stream.Events)
        user.Create(cmd.UserName, ...)
        store.AppendToStream(cmd.UserId, stream.Version, user.Changes)

    Handle(BlockUser cmd)
        stream = store.LoadStream(cmd.UserId)
        user = new User(stream.Events)
        user.Block(string reason)
        store.AppendToStream(cmd.UserId, stream.Version, user.Changes)

总计的:

User
    created = false
    blocked = false

    Changes = new List<Event>

    ctor(eventStream)
        isNewEvent = false
        foreach (event in eventStream)
            this.Apply(event, isNewEvent)

    Create(userName, ...)
        if (this.created) throw "User already exists"
        isNewEvent = true
        this.Apply(new UserCreated(...), isNewEvent)

    Block(reason)
        if (!this.created) throw "No such user"
        if (this.blocked) throw "User is already blocked"
        isNewEvent = true
        this.Apply(new UserBlocked(...), isNewEvent)

    Apply(userCreatedEvent, isNewEvent)
        this.created = true
        if (isNewEvent) this.Changes.Add(userCreatedEvent)

    Apply(userBlockedEvent, isNewEvent)
        this.blocked = true
        if (isNewEvent) this.Changes.Add(userBlockedEvent)

更新:

作为旁注:Yves 的回答让我想起了Udi Dahan几年前的一篇有趣的文章:

丹尼斯优秀答案的一个小变化:

  • 在处理“创建性”用例(即应该剥离新的聚合)时,尝试找到另一个可以将责任转移到的聚合或工厂。 这与使用事件进行水化的 ctor(或任何其他为此进行再水化的机制)并不冲突。 有时工厂只是一个静态方法(适用于“上下文”/“意图”捕获),有时它是另一个聚合的实例方法(“数据”继承的好地方),有时它是一个显式的工厂对象(“复杂”的创建逻辑)。
  • 我喜欢在我的聚合上提供一个显式的 GetChanges() 方法,该方法将内部列表作为数组返回。 如果我的聚合要在一次执行后保留在内存中,我还会添加一个 AcceptChanges() 方法来指示应该清除内部列表(通常在将事物刷新到事件存储后调用)。 您可以在此处使用基于拉取(GetChanges/Changes)或推送(认为 .net 事件或 IObservable)的模型。 很大程度上取决于事务语义、技术、需求等......
  • 您的事件流是一个链表。 每个修订(事件/变更集)都指向前一个(又名父)。 您的事件流是发生在特定聚合上的一系列事件/更改。 顺序仅在聚合边界内得到保证。

几乎同意 yves-reynhout 和 dennis-traub,但我想向您展示我是如何做到这一点的。 我想剥夺我的聚合体的责任,将这些事件应用到自己身上或给自己补充水分; 否则会有很多代码重复:每个聚合构造函数看起来都一样:

UserAggregate:
    ctor(eventStream)
         foreach (event in eventStream)
            this.Apply(event)


OrderAggregate:
    ctor(eventStream)
         foreach (event in eventStream)
            this.Apply(event)


ProfileAggregate:
    ctor(eventStream)
         foreach (event in eventStream)
            this.Apply(event)

这些责任可以留给指挥调度员。 该命令由聚合直接处理。

Command dispatcher class

    dispatchCommand(command) method:
        newEvents = ConcurentProofFunctionCaller.executeFunctionUntilSucceeds(tryToDispatchCommand)
        EventDispatcher.dispatchEvents(newEvents)

    tryToDispatchCommand(command) method:
        aggregateClass = CommandSubscriber.getAggregateClassForCommand(command)
        aggregate = AggregateRepository.loadAggregate(aggregateClass, command.getAggregateId())
        newEvents = CommandApplier.applyCommandOnAggregate(aggregate, command)
        AggregateRepository.saveAggregate(command.getAggregateId(), aggregate, newEvents)

ConcurentProofFunctionCaller class

    executeFunctionUntilSucceeds(pureFunction) method:
        do this n times
            try
                call result=pureFunction()
                return result
            catch(ConcurentWriteException)
                continue
        throw TooManyRetries    

AggregateRepository class

     loadAggregate(aggregateClass, aggregateId) method:
         aggregate = new aggregateClass
         priorEvents = EventStore.loadEvents()
         this.applyEventsOnAggregate(aggregate, priorEvents)

     saveAggregate(aggregateId, aggregate, newEvents)
        this.applyEventsOnAggregate(aggregate, newEvents)
        EventStore.saveEventsForAggregate(aggregateId, newEvents, priorEvents.version)

SomeAggregate class
    handleCommand1(command1) method:
        return new SomeEvent or throw someException BUT don't change state!
    applySomeEvent(SomeEvent) method:
        changeStateSomehow() and not throw any exception and don't return anything!

请记住,这是从 PHP 应用程序投射的伪代码; 真正的代码应该注入一些东西,并在其他类中重构其他职责。 想法是保持聚合尽可能干净并避免代码重复。

关于聚合的一些重要方面:

  1. 命令处理程序不应更改状态; 他们产生事件或抛出异常
  2. 事件适用不应抛出任何异常,不应返回任何内容; 他们只改变内部状态

可以在此处找到一个开源 PHP 实现。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM