将DDD与事件采购混合在一起

时间:2017-12-23 16:02:47

标签: php domain-driven-design event-sourcing

我无法理解将DDD与ES混合在一起的概念。我认为事件是域方的一部分。鉴于将它们从存储库发布到外部世界并保持模型纯粹和简单没有问题。但除此之外,必须有可能在特定的聚合上重播它们。这是我的问题发生的地方。我想保持我的域模型纯粹和简单的对象保持lib /框架不可知。

要在聚合上应用过去的事件,聚合必须知道是ES结构的一部分(因此它不会保留纯域对象)。由于聚合的主要工作是使一些可能随着时间的推移而发展的商用不变量,因此不可能使用聚合API来应用旧事件。例如,有子实体注释的聚合帖子。今天Post允许添加10个评论,方法addCommnet()保护该规则。但它不是一直都是这样的。一年前,用户被允许添加多达20条评论。因此,应用过去的事件可能不符合现行规则。

Broadway(流行的PHP CQRS库)通过应用没有任何预验证的事件来解决问题。方法addCommnet()只是根据我们的不变量进行检查,然后处理应用事件。 Applyinig事件本身不会进行任何进一步检查。这是很好的但我认为我的域模型中的高度集成。我的域模型真的需要了解有关infastructure的任何信息(这是保存数据的ES风格)吗?

编辑: 用尽可能简单的词来陈述问题:有没有机会从聚合中删除所有那些applyXXX()方法?

EDIT2: 我用PHP编写了这个想法的PoC(有点hacky) - github

4 个答案:

答案 0 :(得分:2)

  

我无法理解将DDD与CQRS混合在一起的概念。

从事物的声音来看,你无法完全理解DDD和事件采购的混合。 CQRS和事件采购是一个单独的想法(碰巧很好地结合在一起)。

  

今天发布允许添加10个评论,方法addCommnet()保护该规则。但它不是一直都是这样的。一年前,用户被允许添加多达20条评论。因此,应用过去的事件可能不符合现行规则。

这绝对是真的。但是请注意,如果您有一条带有15条评论的事件来源帖子,并且您现在尝试制定一条“规则”,即只允许10条评论,那么您仍然拥有问题

我对这个谜题的回答(两种风格)都是你需要对所涉及的责任略有不同的理解。

域模型的责任是行为;它描述了哪些状态可以从当前状态到达。域模型不应该限制您处于状态,它应该阻止成为坏状态的良好状态。

在版本1中,我们可能会说Post的状态包含TwentyList of Comments,其中TwentyList是(惊讶)容器,最多可容纳20个注释标识符。

在第二版中,我们希望保留10条评论的限制,我们不会将TwentyList更改为TenList,因为这会让我们产生向后兼容性问题。相反,我们将域规则更改为“没有评论可以添加到包含10个或更多评论的帖子”。数据架构未更改,不良状态仍可表示,但允许的状态转换受到很大限制。

具有讽刺意味的是,Greg Young的Versioning in an Event Sourced System是一本好读的书,可以获得更多的见解。从高层次来看,课程是事件版本控制只是消息版本控制,状态只是以前模型留给当前模型的消息。

值类型与规则约束无关,它们与语义约束有关。

请记住,时间表非常不同;行为是关于现在 next ,但状态是关于过去。国家应该忍受比行为更长的时间(相应的设计资本投资意味着)。

  

我的域模型真的需要了解基础设施(保存数据的ES风格)吗?

不,域模型不需要了解基础架构。

但事件不是基础设施 - 它们是AddCommentRemoveComment个事件的日志状态就像Comment条目的列表一样状态。

最常见的“行为”形式是一种将当前状态作为输入并将事件作为输出发出的函数

List<Event> act(State currentState);

因为我们总是可以在外层,接受事件(这是状态的非破坏性表示,并从中构建状态。

State act(State currentState) {
    List<Event> changes = act(currentState)
    State nextState = currentState.apply(changes)
    return nextState
}

List<Event> act(List<Event> history) {
    State initialState = new State();
    State currentState = initialState.apply(changes)
    return act(currentState)
}

State act(List<Event> history) {
    // Writing this out long hand to drive home the point
    // we could of course call act: List<Event> -> State
    // to avoid duplication.

    List<Event> changes = act(history)
    State initialState = new State()
    State currentState = initialState.apply(history)

    State nextState = currentState.apply(changes)

    return nextState;
}

关键是您可以在最常见的情况下实现行为,添加一些适配器,然后让管道选择最合适的实现。

同样,职责分离是你的指导明星:管理事物的状态,管理允许变更的行为,以及管道/基础设施都是不同的关注点。

  

用最简单的术语来说:我正在寻找机会从我的聚合中摆脱许多applyXXX()(或类似于重载方法的语言)方法

applyXXX只是一个函数,它接受StateEvent作为参数并返回一个新的State。您可以使用任何您想要的拼写和范围。

答案 1 :(得分:2)

免责声明:我是CQRS framework人。

  

Broadway(流行的PHP CQRS库)通过应用没有任何预验证的事件来解决问题。

这就是每个CQRS聚合的工作方式,不检查事件,因为它们表达过去已经发生的事实。这意味着应用event并不会抛出exceptions

  

要在聚合上应用过去的事件,聚合必须知道是ES结构的一部分(因此它不会保留纯域对象)

不,它没有。它必须意识到它过去的事件。那很好。

  

今天发布允许添加10个评论,方法addCommnet()保护该规则。但它不是一直都是这样的。一年前,用户被允许添加多达20条评论。因此,应用过去的事件可能不符合现行规则。

是什么让你{em>忽略 忽略该事件或与1年前不同的解释?! 这种特殊情况应该让您考虑CQRS的强大功能:写入具有与读取不同的逻辑。您可以在聚合上应用事件,以便验证到达它的未来命令(写入/命令端)。显示这20个事件由其他逻辑(读/查询端)处理。

  

这是我的问题发生的地方。我想保持我的域模型纯粹和简单的对象保持lib /框架不可知。

CQRS可以使您的聚合保持纯净(没有副作用),不依赖于任何库和简单。我使用cqrs.nu提供的样式,通过产生事件来做到这一点。这意味着聚合命令处理程序方法实际上是generators

读取模型也可以非常非常简单,简单的PHP不可变对象。只有读取模型更新程序依赖于持久性,但可以使用aggregate反转。

答案 2 :(得分:0)

我的答案非常简短。事实上,这是你挣扎的事件来源,而不是CQRS。

如果某些事件的处理随着时间的推移而发生变化,那么您确实有两种情况:

  1. 你正在修复一个错误,你的处理程序应该有不同的行为。在这种情况下,您只需继续进行更改。
  2. 你有一些新的意图。你实际上有一个新的处理。这意味着事实上这是一个不同的事件。在这种情况下,您有一个新事件和新处理程序。
  3. 这些场景与编程语言和框架无关。事件采购通常更多地是关于任何技术的业务。

    我会支持格雷格的书推荐。

答案 3 :(得分:0)

我认为您的问题是您希望在应用事件时验证事件,但应用和验证是集合操作的两个不同阶段。当你通过方法addComment(event)添加注释时,你的逻辑是有效的,而且这个方法正在抛出事件,当你回复事件时,这个逻辑不会再次检查。过去的事件无法更改,如果您的聚合在回复事件中抛出异常,则您的聚合有问题。这就是我如何理解你的问题。