DDD 和 CQRS/ES 不会打破 DDD 的持久性不可知论吗?
Doesn't DDD and CQRS/ES break the persistence agnosticity of DDD?
DDD 中的域模型应该与持久性无关。
CQRS 指示我为我不想在我的读取模型中拥有的所有内容触发事件。 (顺便说一句,将我的模型拆分为写入模型和至少一个读取模型)。
ES 要求我为所有改变状态的东西触发事件,并且我的聚合根必须自己处理事件。
这对我来说似乎不是持久性不可知论者。
那么如何将 DDD 和 CQRS/ES 结合起来而不会使这种持久性技术对域模型产生重大影响?
读模型是否也在DDD领域模型中?还是外面?
CQRS/ES 事件与 DDD 域事件相同吗?
编辑:
我从答案中提取的内容如下:
是的,对于 ORM,域模型对象的实现将不同于使用 ES。
问题是错误的方法。首先编写领域模型对象,然后决定如何持久化(更多事件像 => ES,更多数据像 => ORM,...)。
但我怀疑如果你没有预先做出这个决定,你将永远能够使用 ES(没有大 additions/changes 到你的域对象),并且在没有预先决定的情况下使用 ORM会造成很大的痛苦。 :-)
你问题中的很多前提都非常 binary/black-and-white。我不认为 DDD、CQRS 或事件溯源是规定性的——有许多可能的解释和实现。
就是说,只有你的前提之一困扰我(强调我的):
ES dictates me to fire events for everything that changes state and
that my aggregate roots must handle the events itself.
通常 AR 会发出事件——它们不会处理这些事件。
在任何情况下,CQRS 和 ES 都可以实现为完全与持久性无关(通常是)。事件以流的形式存储,可以存储在关系数据库、NoSQL 数据库、文件系统、内存等中。事件的存储通常在应用程序的边界实现(我认为这是基础设施),领域模型不知道它们的流是如何存储的。
同样,读取模型可以存储在任何可以想象的存储介质中。您可以有 10 种不同的读取模型和投影,每一种都存储在不同的数据库和不同的格式中。投影只是 handle/read 事件流,否则完全与域分离。
没有比这更不可知的持久性了。
命令
归根结底,CQRS 意味着您应该将读取与写入分开。
通常,一条命令到达系统并由某种函数处理,然后 returns 零个、一个或多个由该命令产生的事件:
handle : cmd:Command -> Event list
现在你有了一个事件列表。您所要做的就是将它们保存在某个地方。执行此操作的函数可能如下所示:
persist : evt:Event -> unit
但是,这样的 persist 功能纯粹是一个基础设施问题。客户端通常只会看到一个将命令作为输入的函数,returns 什么都没有:
attempt : cmd:Command -> unit
其余的(handle
,后跟 persist
)是异步处理的,因此客户端永远不会看到这些函数。
查询
给定一个事件列表,您可以重播它们以便将它们聚合成所需的结果。这样的函数本质上看起来像这样:
query : target:'a -> events:Event list -> Result
给定事件列表和要查找的目标(例如 ID),这样的函数可以将事件折叠成结果。
执着无知
这会迫使您使用特定类型的持久性吗?
None 这些功能是根据任何特定的持久性技术定义的。您可以使用
实现这样的系统
- 内存列表
- 演员
- 活动商店
- 文件
- 斑点
- 数据库,甚至
- 等等
从概念上讲,它确实迫使您从事件的角度考虑持久性,但这与强制您从实体的角度考虑持久性的 ORM 方法没有什么不同和关系。
这里的要点是,将 CQRS+ES 架构与大多数实现细节分离是非常容易的。这通常是持久的无知足够。
不确定这是否正统,但我当前的事件源实体模型做了类似的事情,这可能说明了差异。 . . (C# 示例)
public interface IEventSourcedEntity<IEventTypeICanRespondTo>{
void When(IEventTypeICanRespondTo event);
}
public interface IUser{
bool IsLoggedIn{get;}
}
public class User : IUser, IEventSourcedEntity<IUserEvent>{
public bool IsLoggedIn{get;private set;}
public virtual void When(IUserEvent event){
if(event is LoggedInEvent){
IsLoggedIn = true;
}
}
}
非常简单的示例 - 但您可以在此处看到事件的持久化方式(甚至如果)在域对象之外。您可以通过存储库轻松地做到这一点。同样,CQRS 也受到尊重,因为我读取值的方式与我设置它的方式是分开的。因此,例如,假设我有多个设备供用户使用,并且只希望他们在超过两个设备时登录?
public class MultiDeviceUser : IUser, IEventSourcedEntity<IUserEvent>{
private IEnumerable<LoggedInEvent> _logInEvents = . . .
public bool IsLoggedIn{
get{
return _logInEvents.Count() > MIN_NUMBER_OF_LOGINS;
}
}
public void When(IUserEvent ev){
if(ev is LoggedInEvent){
_logInEvents.Add(ev);
}
}
}
不过,对于调用代码,您的操作是相同的。
var ev = new LoggedInEvent();
user.When(ev);
if(user.IsLoggedIn) . . . .
我认为您仍然可以通过使用卫星 POCO 将您的域与持久性机制分离。然后您可以围绕该 POCO 实施您的特定持久性机制,并让您的域将其用作 snapshot/memento/state.
DDD 中的域模型应该与持久性无关。
CQRS 指示我为我不想在我的读取模型中拥有的所有内容触发事件。 (顺便说一句,将我的模型拆分为写入模型和至少一个读取模型)。
ES 要求我为所有改变状态的东西触发事件,并且我的聚合根必须自己处理事件。
这对我来说似乎不是持久性不可知论者。
那么如何将 DDD 和 CQRS/ES 结合起来而不会使这种持久性技术对域模型产生重大影响?
读模型是否也在DDD领域模型中?还是外面?
CQRS/ES 事件与 DDD 域事件相同吗?
编辑:
我从答案中提取的内容如下:
是的,对于 ORM,域模型对象的实现将不同于使用 ES。 问题是错误的方法。首先编写领域模型对象,然后决定如何持久化(更多事件像 => ES,更多数据像 => ORM,...)。
但我怀疑如果你没有预先做出这个决定,你将永远能够使用 ES(没有大 additions/changes 到你的域对象),并且在没有预先决定的情况下使用 ORM会造成很大的痛苦。 :-)
你问题中的很多前提都非常 binary/black-and-white。我不认为 DDD、CQRS 或事件溯源是规定性的——有许多可能的解释和实现。
就是说,只有你的前提之一困扰我(强调我的):
ES dictates me to fire events for everything that changes state and that my aggregate roots must handle the events itself.
通常 AR 会发出事件——它们不会处理这些事件。
在任何情况下,CQRS 和 ES 都可以实现为完全与持久性无关(通常是)。事件以流的形式存储,可以存储在关系数据库、NoSQL 数据库、文件系统、内存等中。事件的存储通常在应用程序的边界实现(我认为这是基础设施),领域模型不知道它们的流是如何存储的。
同样,读取模型可以存储在任何可以想象的存储介质中。您可以有 10 种不同的读取模型和投影,每一种都存储在不同的数据库和不同的格式中。投影只是 handle/read 事件流,否则完全与域分离。
没有比这更不可知的持久性了。
命令
归根结底,CQRS 意味着您应该将读取与写入分开。
通常,一条命令到达系统并由某种函数处理,然后 returns 零个、一个或多个由该命令产生的事件:
handle : cmd:Command -> Event list
现在你有了一个事件列表。您所要做的就是将它们保存在某个地方。执行此操作的函数可能如下所示:
persist : evt:Event -> unit
但是,这样的 persist 功能纯粹是一个基础设施问题。客户端通常只会看到一个将命令作为输入的函数,returns 什么都没有:
attempt : cmd:Command -> unit
其余的(handle
,后跟 persist
)是异步处理的,因此客户端永远不会看到这些函数。
查询
给定一个事件列表,您可以重播它们以便将它们聚合成所需的结果。这样的函数本质上看起来像这样:
query : target:'a -> events:Event list -> Result
给定事件列表和要查找的目标(例如 ID),这样的函数可以将事件折叠成结果。
执着无知
这会迫使您使用特定类型的持久性吗?
None 这些功能是根据任何特定的持久性技术定义的。您可以使用
实现这样的系统- 内存列表
- 演员
- 活动商店
- 文件
- 斑点
- 数据库,甚至
- 等等
从概念上讲,它确实迫使您从事件的角度考虑持久性,但这与强制您从实体的角度考虑持久性的 ORM 方法没有什么不同和关系。
这里的要点是,将 CQRS+ES 架构与大多数实现细节分离是非常容易的。这通常是持久的无知足够。
不确定这是否正统,但我当前的事件源实体模型做了类似的事情,这可能说明了差异。 . . (C# 示例)
public interface IEventSourcedEntity<IEventTypeICanRespondTo>{
void When(IEventTypeICanRespondTo event);
}
public interface IUser{
bool IsLoggedIn{get;}
}
public class User : IUser, IEventSourcedEntity<IUserEvent>{
public bool IsLoggedIn{get;private set;}
public virtual void When(IUserEvent event){
if(event is LoggedInEvent){
IsLoggedIn = true;
}
}
}
非常简单的示例 - 但您可以在此处看到事件的持久化方式(甚至如果)在域对象之外。您可以通过存储库轻松地做到这一点。同样,CQRS 也受到尊重,因为我读取值的方式与我设置它的方式是分开的。因此,例如,假设我有多个设备供用户使用,并且只希望他们在超过两个设备时登录?
public class MultiDeviceUser : IUser, IEventSourcedEntity<IUserEvent>{
private IEnumerable<LoggedInEvent> _logInEvents = . . .
public bool IsLoggedIn{
get{
return _logInEvents.Count() > MIN_NUMBER_OF_LOGINS;
}
}
public void When(IUserEvent ev){
if(ev is LoggedInEvent){
_logInEvents.Add(ev);
}
}
}
不过,对于调用代码,您的操作是相同的。
var ev = new LoggedInEvent();
user.When(ev);
if(user.IsLoggedIn) . . . .
我认为您仍然可以通过使用卫星 POCO 将您的域与持久性机制分离。然后您可以围绕该 POCO 实施您的特定持久性机制,并让您的域将其用作 snapshot/memento/state.