我浏览了一些用各种不同的通用语言编写的流行的Event Sourcing框架。我得到的印象是所有这些对域模型的影响非常高。据我所知,ES只是一个基础设施问题 - 一种持久化聚合状态的方法。当然,它有助于消息驱动的上下文集成,但在核心域中的观点可以忽略不计。我认为命令和事件是域本身的一部分,因此聚合创建事件(但不发布它们)或处理命令看起来非常好。
问题是所有DDD构建块都倾向于被ES框架污染。事件必须从某个基类继承。聚合至少应该实现外部接口。我想知道域模型是否应该知道在应用程序中使用ES方法。在我看来,甚至提供apply()
方法的必要性表明其他层形成了我们的域。
您如何在项目中解决此问题?
答案 0 :(得分:3)
我的回答仅适用于涉及CQRS(写入和读取模型被拆分并且他们使用域事件进行通信)。
据我所知,ES只是一个基础设施问题 - 一种持久化聚合状态的方法
事件来源确实是一个基础设施问题,一种存储库,但基于事件的聚合不是。我认为它们是一种建筑风格,与古典风格不同。
因此,Aggregate响应命令,生成零个或多个域事件,这些事件应用于自身以构建其内部(私有)状态,用于决定将来生成哪些事件只是一种不同的思维方式和设计聚合。这是一个完美的有效风格,以及经典风格(不使用事件但只使用对象)或函数式编程风格。
事件源只是意味着每次命令到达Aggregate时,其整个内部状态都是重建而不是从平面持久性加载。当然还有其他巨大优势(!),但它们不会影响聚合的设计。
......但不发布它们......
我喜欢允许我们只使return
(或更好yield
- 聚合命令方法只是generators
!)事件的框架。
事件必须从某个基类继承
令人遗憾的是,某些框架需要这样做但不一定如此。通常,框架需要一种检测事件类的方法。但是,它们可以通过其他方式实现检测事件,而不是使用标记接口。例如,客户端(如在您中)可以提供拒绝非事件类的过滤方法。
但是,在我的框架中有一件事我无法避免(是的,我知道,我有罪,我有一个):Command
界面只有一个方法: getAggregateId
。
聚合至少应该实现外部接口。
同样,与事件一样,这不是必需的。可以为框架提供自定义客户端event-applier-on-aggregates function
,也可以使用约定(即所有事件应用程序方法的格式为apply EventClassNameOrType 。
我想知道域模型是否应该知道在应用程序中使用ES方法
ES不是,但基于事件的是,所以apply
方法必须仍然存在。
答案 1 :(得分:2)
据我所知,ES只是一个基础设施问题 - 一种持久化聚合状态的方法。
不,事件确实是域模型的核心。
从技术上讲,您可以以域无关的方式存储 diffs 。例如,您可以查看聚合并说“这里是更改之前的表示,此处是之后的表示,我们将计算差异并存储它。
补丁和事件之间的区别在于您从域无关拼写切换到特定于域的拼写。这样做通常需要与域模型本身密切相关。
问题是所有DDD构建块都会被ES框架污染。
是的,你在野外找到的例子中有很多垃圾框架。 Sturgeon's Law at work
从功能角度思考域模型可以提供很多帮助。在它的核心,模型的最通用形式是一个接受当前状态作为输入的函数,并返回一个事件列表作为输出。
List<Event> change(State current)
从那里开始,如果你想保存当前状态,你只需将这个函数包装成知道如何进行折叠的东西
State current = ...
List<Event> events = change(current)
State updated = State.fold(current, events)
同样,您可以通过折叠先前的历史来获得当前状态
List<Event> savedHistory = ...
State current = State.reduce(savedHistory)
List<Event> events = change(current)
State updated = State.fold(current, events)
另一种说同样话的方式; “事件”已存在于您的(非事件源)域模型中 - 它们只是隐式。 如果在跟踪这些事件时存在业务价值,那么您应该将域模型的实现替换为使这些事件显式化的实现。然后,您可以决定使用独立于域模型的哪个持久化表示。
我的问题的核心是域事件继承自框架事件并且聚合实现了一些外部接口(来自框架)。怎么避免这个?
有几种可能性。
1)滚动自己:仔细看看框架 - 真正购买你的是什么?如果你的答案“不多”,那么也许你可以不用它。
从我所看到的,这些框架的“胜利”倾向于采用异构的事件集合并为您管理路由。这不是什么 - 但它有点神奇,你可能更乐意将代码显式化,而不是依赖于隐式框架魔法
2)吸收它:如果框架不引人注目,那么接受它强加的权衡并与它们一起生活可能更为实际。在某种程度上,事件框架就像对象关系映射器或数据库;当然,在理论中你应该能够自由地改变它们。在实践中?你多久从这种灵活性的投资中获益?3)接口:如果你稍微眯一下,你可以看到你的域行为通常不依赖于内存表示,而是依赖于域本身的代数。
例如,在域模型中,我们将Money
存入Account
更新其Balance
。我们通常不关心这些是整数,长整数,浮点数还是JSON文档。我们可以用任何满足代数约束的实现来满足模型。
所以你可以使用框架来提供实现(它也恰好具有框架所需的所有钩子);行为只与它自己定义的界面相互作用。
在强类型实现中,这可能会让真正扭曲。例如,在Java中,如果你想要强类型检查,你需要熟悉泛型和类型擦除的魔力。
答案 2 :(得分:0)
对此的真正答案是DDD被高估了。你必须有一个模型来统治它们并不是真的。根据您当前的需要,您可能对世界的状况有不同的看法。应用程序的一部分有一个视图,另一部分 - 完全不同的视图。
换句话说,你的模型不是“什么是”,而是“到目前为止发生了什么”。应用程序的实际数据模型是事件流本身。你从那里得到的所有其他东西。