DDD - 如何对跨聚合的集合强制执行不变量

Posted

技术标签:

【中文标题】DDD - 如何对跨聚合的集合强制执行不变量【英文标题】:DDD - How to enforce invariants on collections across aggregates 【发布时间】:2020-06-08 05:04:47 【问题描述】:

假设我们销售汽车,定制汽车。

客户选择CarModel,然后开始配置CarModel。在我们店里,她只能选择Steeringwheel的颜色。 一些 CarModel 的 SteeringWheels 类型可能比其他车型多。

因此,我们有一个Catalog,其中包含CarModelsSteeringWheels

客户可以创建CarConfiguration。她选择模型,然后从该模型的可用方向盘中选择她喜欢的颜色方向盘。

class Catalog

    public IReadonlyCollection<int> CarModels  get; 
    public IReadonlyCollection<int> SteeringWheels  get; 

    public void RemoveSteeringWheel(int steeringWheelId)
    
        ...
    


class SteeringWheel : AggregateRoot

    public int Id  get; 
    public string Color  get; 
    public decimal Price  get; set; 


class CarModel : AggregateRoot

    public int Id  get; 
    public decimal Price  get; set; 
    public IReadonlyCollection<int> SteeringWheels  get; 

    public void AddSteeringWheel(int steeringWheelId)
    
        ...
    

    public CarOrder CreateCarOrder(int steeringWheelId)
    
        return new CarOrder(...);
    


class CarOrder : AggregateRoot

    public int Id  get; set; 
    public CarConfiguration CarConfiguration  get; set; 


class CarConfiguration : ValueObject

    public int CarModelId  get; set; 
    public int SteeringWheelId  get; set; 

为此,有一个不变量,即一个车型的可用方向盘必须始终存在于目录中。为了强制执行这个不变量,我们必须保护(至少)两种方法:

AddSteeringWheel CarModel;如果Catalog 中可用,我们只能添加SteeringWheel RemoveSteeringWheelCatalog;如果SteeringWheel 没有在任何CarModel 上配置,我们只能删除它。

如何强制执行这个不变量? CarModel 不知道 Catalog 上的 SteeringWheel 系列,Catalog 也不知道 CarModel 的方向盘。

我们可以引入域服务并将存储库注入其中。该服务将能够访问来自两个聚合的数据并能够强制执行不变量。

其他选项是创建导航属性并配置 ORM(在我的例子中为实体框架核心)以显式加载这些关系。

可能还有更多,我现在想不出……

实现这一目标的最优雅/纯ddd/最佳实践选项是什么?

【问题讨论】:

【参考方案1】:

如何对跨聚合的集合强制执行不变量

从根本上说,这里存在分析冲突。当您分发信息时,您就放弃了强制执行组合不变量的能力。

例如:

为了实现这一点,有一个不变量,即车型的可用方向盘必须始终存在于目录中

那么当一个人同时更新 CarConfiguration 和另一个人修改目录时应该发生什么?更改目录后,所有现有配置应该发生什么?

在很多情况下,答案是“这些活动都是允许的,我们稍后会清理差异”;即我们稍后会尝试检测问题,如果我们发现任何问题,就会提出异常报告。

(如果该答案不令人满意,那么您需要回到最初的决定,将信息拆分为多个聚合体,并审查该设计)。

Pat Helland 在这里提供了很多有用的资料:

2009Building on Quicksand 2010Memories Guesses and Apologies 2015Immutability Changes Everything

实际上,您的本地计算包括来自其他地方的陈旧(并且可能是过时)信息,并且您将对此的真正担忧编码到您的逻辑中。

【讨论】:

当您说“我们稍后会清理差异”时,您是指像@DmitriBodiu 所说的毫秒后吗?或者可能会在很久以后由人工操作员解决问题? 可能是毫秒、分钟或月,具体取决于成本/收益分析。【参考方案2】:

首先,CarModel 可能知道SteeringWheels 的某些内容,因为我假设如果您添加SteeringWheelPriceCarModelPrice 会发生变化?!

所以可能应该有一个值对象或实体作为代表它的CarModel 聚合的一部分。

此外,我认为您需要一个命令处理程序,它知道两者,并确定提供的SteeringWheel 是否有效,然后再尝试将其添加到CarModel,它自己必须决定是否添加SteeringWheel 是允许的,相信命令处理程序 SteeringWheel 的引用是有效的。

【讨论】:

我同意。我可能会将它建模为一个值对象。该方法将接受值对象,而不是 id。调用该方法的客户端将负责构造给定 id 的值对象。 VO 工厂可以接受一个域服务,该服务将从方向盘模型中检索必要的细节。要删除,需要另一个域服务。 是的,我正在使用命令和处理程序。并且这些命令已经过验证(甚至针对数据库)。所以除了并发请求之外,不会有问题。这个问题更多的是关于那些并发请求;如果一个人通过了怎么办,如何以最类似 ddd 的方式处理它。【参考方案3】:

聚合之间的不变量不能过渡一致,只有最终一致。因此,当您将方向盘添加到您的 carModel 时,您会引发一个事件,说它是指轮转轮使用的 CarModelEvent,您会在域事件处理程序中捕获该事件并更新方向盘。 Steering Wheel 聚合保存分配给它的汽车模型的 id(或集合,如果可以由多个汽车配置使用)。

【讨论】:

如果更新破坏了不变量,我可以在事件处理程序中抛出异常?但是随后我们在应用层而不是在域中强制执行不变量。我现在明白了你的观点,即聚合之间的不变量只能是最终的。我认为... :) "如果更新破坏了不变量,我可以在事件处理程序中抛出异常?" - 为什么在事件处理程序中?事件处理程序调用应用层。应用层调用域,然后域检查不变量 是的,最终意味着,当事件正在传播时,(通常为几毫秒)其他一些事务可能会改变 Aggregate2 的状态,这将导致您的操作失败

以上是关于DDD - 如何对跨聚合的集合强制执行不变量的主要内容,如果未能解决你的问题,请参考以下文章

DDD:关于聚合边界的问题

DDD领域驱动设计实战-聚合(Aggregate)和聚合根(AggregateRoot)

对跨标准和变体类型列的查询执行聚合函数

接口自动化 测试数据驱动 DDD模块使用

找到算法的循环不变量

在 Objective-C 中维护代表不变量