Domain driven design 事件源:回滚聚合状态的正确方法

Domain driven design 事件源:回滚聚合状态的正确方法,domain-driven-design,cqrs,event-sourcing,Domain Driven Design,Cqrs,Event Sourcing,我正在寻找与在CQRS/事件源应用程序中实现回滚功能的正确方法相关的建议 此应用程序允许一组编辑编辑和更新某些编辑内容,例如编辑新闻。我们实现了用户界面,因此每个字段都有一个自动保存功能,现在我们希望为用户提供撤消他们所做操作的可能性,以便能够将编辑新闻回滚到以前的已知状态。 基本上,我们希望实现类似于Microsoft Word和类似文本编辑器中的撤消命令的功能。在后端,编辑新闻是在我们的域中定义的聚合的一个实例,称为Story 我们已经讨论了一些实现回滚的想法,我们正在根据类似项目的实际经验

我正在寻找与在CQRS/事件源应用程序中实现回滚功能的正确方法相关的建议

此应用程序允许一组编辑编辑和更新某些编辑内容,例如编辑新闻。我们实现了用户界面,因此每个字段都有一个自动保存功能,现在我们希望为用户提供撤消他们所做操作的可能性,以便能够将编辑新闻回滚到以前的已知状态。
基本上,我们希望实现类似于Microsoft Word和类似文本编辑器中的撤消命令的功能。在后端,编辑新闻是在我们的域中定义的聚合的一个实例,称为Story

我们已经讨论了一些实现回滚的想法,我们正在根据类似项目的实际经验寻求建议。下面是我们对这个特性的考虑

回滚在实际业务领域中的工作原理 首先,我们都知道,在现实世界的业务领域中,我们所谓的回滚是通过某种形式的补偿事件获得的

设想一个与某种服务相关的域,可以为其购买订阅:我们可以有一个表示用户订阅的聚合和一个描述费用已与聚合实例(其中一个客户的特定订阅)关联的事件。该活动的可能实施方式如下:

public class ChargeAssociatedToSubscriptionEvent: DomainEvent
{
  public Guid SubscriptionId {get; set;}
  public decimal Amount {get; set;}
  public string Description {get; set;}
  public DateTime DueDate {get; set;}
}
如果收费错误地与订阅关联,则可以通过与同一订阅关联并具有相同金额的认证来修复错误,从而使收费的效果完全平衡,用户可以拿回自己的钱。换句话说,我们可以定义以下补偿事件:

public class AccreditationAssociatedToSubscription: DomainEvent
{
  public Guid SubscriptionId {get; set;}
  public decimal Amount {get; set;}
  public string Description {get; set;}
  public DateTime AccreditationDate {get; set;}
}
因此,如果一个用户被错误地收取了50美元的费用,我们可以通过对用户订阅进行50美元的认证来补偿错误:这样,聚合的状态已经回滚到以前的状态

为什么事情不像看上去那么容易 根据前面的讨论,回滚似乎很容易实现。如果在聚合修订版B中有一个故事聚合实例,并且希望将其回滚到以前的聚合修订版,例如a(a
  • 检查事件存储并获取修订版A和B之间的所有事件
  • 计算每个已发生事件的补偿事件
  • 按相反顺序将补偿事件应用于聚合
不幸的是,前面过程的第二步并不总是可能的:给定一个通用域事件,并不总是能够计算其补偿事件,因为事件中包含的信息量不足以计算补偿事件。也许可以明智地定义所有事件,以便它们包含足够的信息,能够计算相应的补偿事件,但在应用程序的当前状态下,有几个事件无法计算补偿事件,我们希望避免更改事件的形状

基于状态比较的可能解决方案 克服补偿事件问题的第一个想法是通过比较聚合的当前状态与目标状态来计算回滚聚合所需的最小事件集。算法基本如下:

  • 获取当前状态下聚合的实例(称为B)
  • 通过仅应用事件存储区中保存的前n个事件(我们的存储库允许通过指定聚合id和将聚合具体化到的所需时间点),在目标状态(称为A)获取聚合实例
  • 比较这两个实例,并计算要应用于处于状态B的聚合的最小事件集,以便将其状态更改为A
  • 将计算出的事件应用于聚合
一种基于事件回放的智能方法 解决回滚到聚合的先前状态的另一种方法是执行与聚合存储库在特定时间点具体化聚合时相同的操作。为了做到这一点,我们应该定义一个事件,比如StoryResettedEvent,其作用是通过完全清空它来重置聚合的状态,并执行以下步骤:

  • 将StoryResettedEvent应用于聚合,使其状态为空
  • 获取我们正在处理的聚合的前n个事件(从第一个保存的事件到目标状态A的所有事件)
  • 将所有事件应用于聚合实例
我看到这种方法的主要问题是清空聚合状态的事件:它似乎有些人为,不是具有业务意义的真正域事件,而是实现回滚功能的技巧

第三种方法:每次在事件存储中保存事件时,都要持久化补偿事件 我们找到的第三种获得所需的方法是再次基于补偿事件的概念。基本思想是,应用程序的每个事件都可以通过包含相应补偿事件的属性来丰富

在引发事件的代码中,可以立即计算要引发的事件的补偿事件(基于聚合的当前状态和事件的形状),以便使用此信息丰富事件,从而将此方式保存在事件存储中。这样,补偿事件始终可用,随时可用