我无法理解将DDD与ES混合在一起的概念。我认为事件是域方的一部分。鉴于将它们从存储库发布到外部世界并保持模型纯粹和简单没有问题。但除此之外,必须有可能在特定的聚合上重播它们。这是我的问题发生的地方。我想保持我的域模型纯粹和简单的对象保持lib /框架不可知。
要在聚合上应用过去的事件,聚合必须知道是ES结构的一部分(因此它不会保持纯域对象)。由于聚合的主要工作是使一些可能随着时间的推移而发展的商用不变量,因此不可能使用聚合API来应用旧事件。例如,有子实体注释的聚合帖子。今天Post允许添加10个评论,方法addCommnet()保护该规则。但它不是一直都是这样的。一年前,用户被允许添加多达20条评论。因此,应用过去的事件可能不符合当前的规则。
百老汇(流行的PHP CQRS库)通过应用没有任何预验证的事件来解决问题。方法addCommnet()只是根据我们的不变量进行检查,然后处理应用事件。 Applyinig事件本身不会进行任何进一步检查。这是很好的但我认为我的域模型中的高度集成。我的域模型真的需要了解有关infastructure的任何信息(这是保存数据的ES风格)吗?
编辑:用尽可能简单的词来陈述问题:有没有机会摆脱聚合中的所有applyXXX()方法?
EDIT2:我用PHP编写了这个想法的PoC(有点hacky) - github
免责声明:我是一个CQRS framework家伙。
百老汇(流行的PHP CQRS库)通过应用没有任何预验证的事件来解决问题。
这就是每个CQRS聚合的工作方式,事件不会被检查,因为它们表达了过去已经发生的事实。这意味着应用event
不会抛出exceptions
。
要在聚合上应用过去的事件,聚合必须知道是ES结构的一部分(因此它不会保留纯域对象)
不,它没有。它必须意识到它过去的事件。那很好。
今天Post允许添加10个评论,方法addCommnet()保护该规则。但它不是一直都是这样的。一年前,用户被允许添加多达20条评论。因此,应用过去的事件可能不符合当前的规则。
是什么让你让aggregate
无视这个事件或者解释不同于1年前?!这种特殊情况应该让您考虑CQRS的强大功能:写入具有与读取不同的逻辑。您可以在聚合上应用事件,以便验证到达它的未来命令(写入/命令端)。显示这20个事件由其他逻辑(读/查询端)处理。
这是我的问题发生的地方。我想保持我的域模型纯粹和简单的对象保持lib /框架不可知。
CQRS可以使您的聚合保持纯净(没有副作用),不依赖于任何库和简单。我使用cqrs.nu提供的风格,通过产生事件来做到这一点。这意味着聚合命令处理程序方法实际上是generators。
读取模型也可以非常简单,简单的PHP不可变对象。只有读取模型更新程序依赖于持久性,但可以使用interface
反转。
我无法理解将DDD与CQRS混合在一起的概念。
从事物的声音来看,你无法完全了解DDD和事件采购的组合。 CQRS和事件采购是独立的想法(碰巧很好地结合在一起)。
今天Post允许添加10个评论,方法addCommnet()保护该规则。但它不是一直都是这样的。一年前,用户被允许添加多达20条评论。因此,应用过去的事件可能不符合当前的规则。
这绝对是真的。但是请注意,如果您有一个包含15条评论的非事件来源帖子,并且您现在尝试制定“规则”,即只允许10条评论,那么您仍然遇到问题。
我对这个难题的回答(两种风格)都是你需要对所涉及的责任略有不同的理解。
域模型的责任是行为;它描述了哪些状态可以从当前状态到达。域模型不应该限制您处于错误状态,它应该防止好状态变为坏状态。
在第一版中,我们可以说Post
的状态包括TwentyList of Comments
,其中TwentyList是(惊讶)一个容器,最多可以容纳20个注释标识符。
在版本2中,我们希望保持10个注释的限制,我们不会将TwentyList
更改为TenList
,因为这会给我们带来向后兼容性的麻烦。相反,我们将域规则更改为“没有评论可以添加到包含10个或更多评论的帖子”。数据模式没有改变,并且不期望的状态仍然是可表示的,但是允许的状态转换受到很大限制。
具有讽刺意味的是,Greg Young的Versioning in an Event Sourced System是一本值得阅读以获得更多见解的好书。从高层次来讲,课程是事件版本控制只是消息版本控制,而状态只是前一个模型留给当前模型的消息。
值类型不是关于规则约束,而是关于语义约束。
请记住,时间表是非常不同的;行为是关于现在和下一个,但状态是关于过去。国家应该忍受比行为更长的时间(相应的设计资本投资意味着)。
我的域模型真的需要了解基础设施(保存数据的ES风格)吗?
不,域模型不需要了解基础架构。
但事件不是基础设施 - 它们是国家。 AddComment
和RemoveComment
事件的日志状态就像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
只是一个函数,接受State
和Event
作为参数并返回一个新的State
。您可以使用任何您想要的拼写和范围。
我的答案很短。实际上,这是您难以接受的事件采购,而不是CQRS。
如果某些事件的处理随着时间的推移而变化,那么您确实有两种情况
这些场景与编程语言和框架无关。事件采购通常更多地是关于任何技术的业务。
我会支持格雷格的书推荐。
我认为您的问题是您希望在应用它们时验证事件,但应用和验证是聚合操作的两个不同阶段。当你通过方法addComment(event)添加注释时,你的逻辑是有效的,而且这个方法正在抛出事件,当你回复事件时,这个逻辑不会再次检查。过去的事件无法更改,如果您的聚合在回复事件中抛出异常,则您的聚合有问题。这就是我理解你的问题的方式。