领域驱动设计:如何设计具有依赖关系的关系聚合

Posted

技术标签:

【中文标题】领域驱动设计:如何设计具有依赖关系的关系聚合【英文标题】:Domain-Driven Design: How to design relational aggregates with a dependency 【发布时间】:2017-03-21 21:04:56 【问题描述】:

我的域名是关于项目管理的。我有一个程序(聚合根),它必须有一个客户(聚合根)。所以我在创建一个新程序时需要一个 CustomerID,因为我已经阅读了聚合应该只通过引用保持对其他聚合的引用。

这是我的业务规则:

    客户可以随着时间的推移变得活跃和不活跃。

    如果客户因某种原因被停用,则与该客户关联的所有程序也应停用。

    如果客户处于非活动状态,则无法激活程序。

规则 #1 & #2 我已实施。难倒我的是#3。

我能想到 3 个解决方案:

    程序持有对客户聚合的引用。

    引入一个域服务来检查客户是否处于活动状态并将其传递给 Program.Activate(CustomerActiveCheckService 服务)。

    让应用程序服务查找客户并将其传递给 Program.Activate(Customer customer)。

哪个是最好的解决方案?

更新

我看到@ConstaninGALBENU 和@plalx 的两种观点,我想提出一个折衷方案。我可以创建CustomerStatusChecker 服务吗?该方法将具有以下签名:CustomerStatus CheckStatus(CustomerID id); 然后我可以像这样传递Program服务:`Program.Activate(CustomerStatusChecker service);

这个设计有什么问题吗?

【问题讨论】:

【参考方案1】:

哪个是最好的解决方案?

没有最佳解决方案;有取舍。

但与要求 #2 和 #3 一致的一种可能的解决方案是,您现有的模型是错误的 - Program 实体不是孤立的聚合,而是 Customer 实体的一部分,因此应该由相同的控制聚合根。

提示可能是这种情况:程序的生命周期适合客户的生命周期;程序通常不会从一个客户迁移到另一个客户,每个客户的活动程序数量是有限的。

另一种可能性是需求是“错误的”。探索这一点的一种方法是查看活动/非活动是否是由模型做出的决定,或者它是否是在其他地方做出并报告给模型的决定。另一个是检查如果违反此“规则”对企业的成本。

如果模型没有立即发现客户,或者这是一个廉价的问题,那么您可能有一些空间来检测冲突并将其报告给人类,而不是试图让模型完成所有工作作品(参见:Greg Young,Stop Over Engineering)。

在这些情况下,让主代码路径可以很好地猜测,并实现操作员可以用来修复错误的替代路径。

在解决方案 #2 和 #3(我根本不喜欢 #1)之间进行选择时,我鼓励将 I/O 操作排除在模型之外。因此,除非您已经在内存中拥有最新版本的客户,否则我不喜欢将域服务作为选择。将客户状态的副本传递给域模型将 I/O 关注点保留在它们所属的应用程序组件中(有关此想法的更多信息,请参阅 Gary Bernhardt 的 Boundaries)。

【讨论】:

我不会太担心模型在这里出错。正如 Vaugn Vernon 所建议的那样,保持 AR 很小;业务需求可能会发生变化,较小的 AR 可以更好地应对变化。 关于设计的决定基于几件事。首先,程序负责维护随时间变化的需求。其次,如果我让客户成为 AR over Program,我必须对员工遵循相同的逻辑。那么我该如何处理一个跟踪哪些员工在哪些项目中的 ParticipantRoster 对象呢?【参考方案2】:

解决方案 1:它违反了不持有对其他聚合实例的引用的规则。该规则确保在事务中只修改一个聚合。如果您需要在单个事务中修改多个聚合,那么您的设计肯定是错误的。

解决方案 2:我真的不喜欢在聚合中注入服务。我的聚合是纯函数,不涉及外部世界(I/O、存储库等)。

解决方案 3:在某种程度上等同于 1,即使它是临时引用(Program 可以在 Customer 上调用命令方法,从而在与 Program 相同的事务边界中修改 Customer

我的解决方案:在应用程序服务内部进行检查,然后调用 Program.activate () 或将 customerStatus 传递给 Program.activate () 并让 Program 聚合决定它是否引发异常或发出事件。

更新:

这个想法是您应该只将只读/不可变数据传递给Program AR,以确保它不会修改其事务边界中的其他 AR。 另外,我们不应该让Program 依赖于它不需要的东西,就像整个Customer AR

另外,如果架构是事件驱动的,那么通过监听Customer 发出的正确事件,您可以使Program AR 保持同步:如果尚未激活或停用它,则将其设为“不可激活”如果它已经被激活,例如使用Saga

【讨论】:

如果传递一个customerStatus,如何确保传递正确的customerStatus? 我不同意解决方案 #3 等同于 #1,因为很明显 AR 不是边界的一部分,而边界不属于 #1。实际上,Vaughn Vernon 在他的 IDDD 书中都推荐了(#2 和 #3)。 我想说#5 可能是在Program 中维护一个canBeActivated 标志,通过侦听CustomerActivatedCustomerDeactivated 等域事件来保持最新。我想这几乎就是您停用 Program 实例的方式了? @BrianGray 应用服务通过 id 从 repo 加载 正确 客户,然后访问 status 属性。这个想法是将只读值传递给Program,这样它就不会尝试改变其他AR @ConstantinGALBENU 你如何确保什么? Program 不会变异 Customer?好吧,这将是一件很难做的事情,而且几乎必须是故意的,我怀疑程序员会故意引入错误。我可以问同样的问题,您如何确定Program 收到的状态属于正确的客户?

以上是关于领域驱动设计:如何设计具有依赖关系的关系聚合的主要内容,如果未能解决你的问题,请参考以下文章

如何运用领域驱动设计 - 聚合

基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则

领域驱动设计业务架构 与 业务中台的关系

基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则

领域驱动设计和Spring

领域驱动设计和Spring(翻译)