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

时间:2012-09-11 04:10:11

标签: events domain-driven-design cqrs

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

到目前为止我所理解的是:

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

到目前为止,这么好。 现在有一些我还没有得到的问题:

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

3 个答案:

答案 0 :(得分:33)

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

<强> 1。聚合创建:

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

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

如果之前不存在聚合,则流将为空并且新创建的对象将处于其原始状态。您可能希望确保在此状态下只允许一些命令使聚合生命,例如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)
        foreach (event in eventStream)
            this.Apply(event)

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

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

    Apply(userCreatedEvent)
        this.created = true
        this.Changes.Add(userCreatedEvent)

    Apply(userBlockedEvent)
        this.blocked = true
        this.Changes.Add(userBlockedEvent)

<强>更新

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

答案 1 :(得分:11)

Dennis的一个小变化很好的答案:

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

答案 2 :(得分:0)

几乎同意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. command handlers should not change state; they yield events or throw exceptions
  2. 事件适用不应抛出任何异常,不应返回任何内容;他们只改变内部状态
  3. 可以找到here的开源PHP实现。