DDD - 如何对跨聚合的集合强制执行不变量
Posted
技术标签:
【中文标题】DDD - 如何对跨聚合的集合强制执行不变量【英文标题】:DDD - How to enforce invariants on collections across aggregates 【发布时间】:2020-06-08 05:04:47 【问题描述】:假设我们销售汽车,定制汽车。
客户选择CarModel
,然后开始配置CarModel
。在我们店里,她只能选择Steeringwheel
的颜色。
一些 CarModel 的 SteeringWheels 类型可能比其他车型多。
因此,我们有一个Catalog
,其中包含CarModels
和SteeringWheels
。
客户可以创建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
RemoveSteeringWheel
上 Catalog
;如果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
的某些内容,因为我假设如果您添加SteeringWheel
和Price
,CarModel
的Price
会发生变化?!
所以可能应该有一个值对象或实体作为代表它的CarModel
聚合的一部分。
此外,我认为您需要一个命令处理程序,它知道两者,并确定提供的SteeringWheel
是否有效,然后再尝试将其添加到CarModel
,它自己必须决定是否添加SteeringWheel
是允许的,相信命令处理程序 SteeringWheel
的引用是有效的。
【讨论】:
我同意。我可能会将它建模为一个值对象。该方法将接受值对象,而不是 id。调用该方法的客户端将负责构造给定 id 的值对象。 VO 工厂可以接受一个域服务,该服务将从方向盘模型中检索必要的细节。要删除,需要另一个域服务。 是的,我正在使用命令和处理程序。并且这些命令已经过验证(甚至针对数据库)。所以除了并发请求之外,不会有问题。这个问题更多的是关于那些并发请求;如果一个人通过了怎么办,如何以最类似 ddd 的方式处理它。【参考方案3】:聚合之间的不变量不能过渡一致,只有最终一致。因此,当您将方向盘添加到您的 carModel 时,您会引发一个事件,说它是指轮转轮使用的 CarModelEvent,您会在域事件处理程序中捕获该事件并更新方向盘。 Steering Wheel 聚合保存分配给它的汽车模型的 id(或集合,如果可以由多个汽车配置使用)。
【讨论】:
如果更新破坏了不变量,我可以在事件处理程序中抛出异常?但是随后我们在应用层而不是在域中强制执行不变量。我现在明白了你的观点,即聚合之间的不变量只能是最终的。我认为... :) "如果更新破坏了不变量,我可以在事件处理程序中抛出异常?" - 为什么在事件处理程序中?事件处理程序调用应用层。应用层调用域,然后域检查不变量 是的,最终意味着,当事件正在传播时,(通常为几毫秒)其他一些事务可能会改变 Aggregate2 的状态,这将导致您的操作失败以上是关于DDD - 如何对跨聚合的集合强制执行不变量的主要内容,如果未能解决你的问题,请参考以下文章