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.