【问题标题】:DDD Questions about creation and upgrade of aggregate root with child entitities and its persistenceDDD 关于创建和升级具有子实体的聚合根及其持久性的问题
【发布时间】:2021-12-06 23:49:21
【问题描述】:

在我正在开发的应用程序中,用户可以在没有订单行的情况下启动订单, 但如果没有订单行和其他聚合的其他属性,订单将无法持久被引用(在数据库中是不接受空值的外键,这就是原因)。 此订单一直保存在缓存中,直到用户确认,即保留它。

我认为通过其构造函数创建聚合根,使其在持久性方面处于不一致的状态是不可能的。我的想法有问题吗?因为如果这是可能的,我该如何处理它的持久性规则?在应用服务?在您的存储库中?通过聚合根中的函数(可能是“IsValidToPersist”)?

我认为在这种情况下,可以在没有持久性所需的所有属性的情况下创建订单。我通常在聚合根的构造函数中检查这些需求,如果聚合根还没有准备好持久化,则抛出异常,以及其他业务规则。 我猜如果是这样的话,那么一定有像“CreateOrderService”、“AddOrderLineService”和“SaveOrderService”/“AddOrderService”这样的应用程序服务会持续存在。而且不仅仅是一个“AddOrderService” 服务,它同时被创建和持久化。

要更新订单行,我是否应该创建一个“UpdateOrderLineService”服务来查找订单,通过其函数更新其订单行? 还是应该在单个服务“UpdateOrderService”中更新 Oder 所需的全部内容?。我认为这第二个选项使一些事情复杂化。例如,服务很难知道要添加、更新和删除哪些订单行。虽然这可以通过像“ChangeOrderLines”这样的 Order 函数来解决,然后简单地替换它们。在第一个选项中,订单是否应该随着订单行的每次更新(甚至添加或删除)而持久化?我可以考虑其他选项,例如:返回更新的订单或返回更新的(或添加的)订单行(尽管这不是一个好主意)。

我的问题,真的是:存在服务来处理聚合根的子实体是否方便?

编辑(关于应用的具体信息):

我对@rascio 的回复包含有关该应用程序的更有用的具体细节:

应用程序有一个“创建”订单的游览。一屏又一屏,订单被“创建”。用户必须在第一个屏幕上写一个数字,没有这个数字就不能创建订单。必须是在 db 中持久保存的有效数字(例如折扣代码)。之后,用户可以选择产品,然后再选择其他东西。我需要检查并保存用户为每个屏幕写入或选择的数据。最后一个屏幕是订单的摘要,用户可以在其中更改内容并确认订单。这是一个桌面应用程序。无副作用。真的不是订单,但很相似。

编辑 2(有关应用的更多具体信息):

有两种方法可以创建订单。一个经历:介绍号码,产品和其他东西。一屏又一屏。游览完成后,将显示摘要屏幕,用户可以在其中修改和确认。或者您可以从头开始在此摘要屏幕上创建它。您也可以从任何其他游览屏幕转到此摘要屏幕。如果用户在游览中只选择了数量和产品,这些将是出现在摘要中的那些,而不是其他可以在摘要中完成的内容。

【问题讨论】:

  • 您能否详细说明“用户可以在没有订单行的情况下启动订单”,用户启动订单是什么意思?这有什么副作用?这是否意味着如果用户关闭浏览器并从智能手机连接,订单创建后他应该会看到订单?
  • 应用程序有一个“创建”订单的游览。一屏又一屏,订单被“创建”。用户必须在第一个屏幕上写一个数字,没有这个数字就不能创建订单。必须是在 db 中持久保存的有效数字(例如折扣代码)。之后,用户可以选择产品,然后再选择其他东西。我需要检查并保存用户为每个屏幕写入或选择的数据。最后一个屏幕是订单的摘要,用户可以在其中更改内容并确认订单。这是一个桌面应用程序。无副作用。真的不是订单,但很相似。

标签: design-patterns service architecture aggregate domain-driven-design


【解决方案1】:

我建议不要使用跨越聚合边界的外键约束。在 DB 基础架构中强制执行此类约束意味着以下两件事中的至少一项是正确的:

  • 约束未在您的域逻辑中实施,在这种情况下,您的域逻辑未完全捕获您的域
  • 在您的域逻辑中强制执行约束,在这种情况下,您为什么要复制内容?

如果您将ON DELETE CASCADE 之类的东西作为该约束的一部分,那么您也违反了对聚合的所有访问都通过根目录的原则(因为数据库肯定不会通过根目录) ),但如果没有任何操作可能导致聚合的那种“背后”修改,这可能不一定是问题。

在这种情况下,很明显,您的域逻辑(表示“没有行的订单可以”)和您的数据库基础架构(表示“不”)之间存在分歧。如果您不能删除外键约束,那么您应该在域逻辑中复制该约束,并且不允许在没有行的情况下构建订单。这可能需要一个 OrderInProgress 聚合,它可能不具有与 Order 相同的不变量。

【讨论】:

  • 我不认为聚合边界设计错误。也许我没有写关于应用程序的具体信息,以便更好地理解。我认为你的最后一段对这个问题更有意义。请,我邀请你阅读我写的版本。感谢您的回复。 编辑:我考虑你的最后一段。我认为这是解决问题的好方法。
【解决方案2】:

有两种主要的方式来模拟这个 IMO:

  1. Order 总是需要一些订单行,否则就不是Order。在这种情况下,您将使其成为Order 的不变量甚至存在,因此“没有订单行”的东西不是Order。也许是ShoppingCart?也许是OrderDraft?它将被建模为与Order“相似”的概念,最有可能从中创建真正的Order

  2. 您使不变量不仅取决于Order 的存在,还取决于它的状态。例如,您可以将其视为接受的订单必须包含订单项。在这种情况下,订单可能以进行中开始,并且只需要订单项即可过渡到已接受/已提交/等。状态。在这种情况下,规则将适用于例如调用order.accept()

"那么必须有“CreateOrderService”、“AddOrderLineService”和“SaveOrderService”/“AddOrderService”等应用服务

如果每个用例都有一个,我不会称它为“服务”。我称之为XXXUseCaseXXXHandler,但Service 后缀通常用于更广泛的 接口,例如:

class OrderService {
    void create(...) { }
    void addLineItem(...) {}
    //...
}

确保用例名称与域的语言(通用语言)而不是技术术语(例如 saveOrder)保持一致。

【讨论】:

  • 是的,我认为您的选项之一是一个很好的解决方案。我虽然有些类似,比如“OrderCache”,因为这些对象不会被持久化。这与 Levi Ramsey 回复的最后一段相似。感谢名字的建议。我将“控制器”命名为“服务”。感谢您的回复!
【解决方案3】:

我认为您的困惑源于您的 DDD 实体可以直接支持用户工作流的假设。不是这种情况。用户工作流程是 UI 问题,而不是您的聚合问题。

用户可以在没有订单行的情况下启动订单,但订单不能 在没有订单行的情况下持久化

该要求的第一部分是用户体验要求。第二部分是您的领域模型的不变量。

“但这意味着要支持 UI,我需要创建 类似 对象,就像我在域模型中已有的一样!?!”

是的!

此订单一直保存在缓存中,直到用户确认,即 时刻坚持下去。

很好,但是该缓存不属于您的域模型。它属于客户端。

我可能希望在您的限界上下文中看到支持订单处理的服务包括:

  • CreateOrder(只能提供完全一致的订单,不能是空的)
  • AddOrderLine(向现有订单添加一行)
  • UpdateOrderLine(更改现有订单行的详细信息)
  • RemoveOrderLine(如果订单仅剩一行,则会自动删除订单 - 假设您的域规则是订单不能没有行存在)。
  • RemoveOrder(删除订单实体及其所有行)。

但不包括:

  • 保存订单

所有以前的服务将在每个命令完成时持续存在。

存在服务来处理子实体是否方便? 聚合根?

是的,正如我在上面所概述的。但这并没有扩展到通过“正在进行的工作”来帮助用户,在此期间实体可能会违反您的域不变量。这是客户的任务。

【讨论】:

  • 谢谢!我一直听从你的建议。我考虑其他用户的回复。稍后我会写下我是如何解决的。
【解决方案4】:

在我看来,您的域规则希望您的订单在某些州没有订单行。

从您的最新更新看来,要创建的订单聚合仅具有与用户需要输入的数量相关的不变量。

这是您的域逻辑的一部分。

这个订单一直保存在缓存中,直到用户确认它,这就是持久化它的时刻。

这不是一个普通的“软件”缓存,这是你的域想要用宽松的不变量来维护你的顺序,直到用户做某事。

当您“更新”聚合时,它可以更改其内部状态,并且可以在不同的内部状态上应用不同的不变量。

我认为,在这种情况下,可以在没有持久性所需的所有属性的情况下创建 Order。

这对我来说完全正确。

我想如果可以的话,那么一定有像“CreateOrderService”、“AddOrderLineService”和“SaveOrderService”/“AddOrderService”这样的应用程序服务会持续存在。

就您的域而言,我想说的不仅仅是服务,这些似乎是您的聚合应该支持的命令。

看看你写的,在我看来,一个有效的命令序列可以是:

CreateOrder(ANumber) -> AddProduct(Product) -> AddProduct(Product) -> AddOtherThings(OtherThings)

在每个命令之后,聚合的状态会保持不变。
每个命令都需要自己的验证,所以:

  • CreateOrder 需要一个有效的数字(非空等)
  • AddOrderLine 有效产品,订单应该完成(你没有写这个,但我想在你 AddOtherThings 或其他事情完成后,你不能再向订单添加任何产品
  • AddOtherThings 输入有效且订单中至少应包含一种产品

(这是一个示例,还应根据您向用户提供的“以原子方式持久保存的单个更新”来对命令进行建模,因此在某些情况下UpdateOrderLines(OrderLine...)) 也是有意义的。)

如何处理其持久性的规则?在应用服务?在您的存储库中?

我想用这个回应强调一个事实,一个聚合可以有多个状态,每个状态都可以有自己的不变量。 DDD 中实体/聚合的作用是维护这些不变量的真实性,并禁止可能将数据移动到不一致状态的更新。所以这些规则应该在你处理改变聚合内部状态的逻辑的地方处理。

最后一个问题:

我的问题,真的是:存在服务来处理聚合根的子实体是否方便?

这些取决于您所说的“服务”,应用程序服务是什么意思?域服务?还有什么服务?我讨厌“服务”这个词,因为它没有确切的含义:)
顺便说一下,作为软件组件,这取决于您的通用架构方法,但 DDD 建议的是聚合根的聚合“通过”的所有突变,因为要强制执行不变量,您可能需要整个聚合的状态,例如,仅当 Order 实体(聚合根)处于特定状态时,才能更新 OrderLine 实体。
无论您使用什么软件组件,重要的是它们将您的整个聚合视为单个原子数据单元,就像它不支持并发修改一样,因此在每个突变不变量关于聚合的数据可以检查。

【讨论】:

  • 感谢您的回答。你的远见对我帮助很大。为关于聚合的每个可能的用户操作创建一个命令(我称之为(应用程序)服务)似乎是个好主意。但我怀疑该命令是否应该继续存在。仅当用户完成订单时,企业才要求我保存订单。这意味着您必须分配该编号、产品和其他东西,它们都是必需的。如果用户没有完成订单,则不会保存。用户还可以在摘要屏幕上修改所有这些选项,甚至可以从此屏幕开始创建订单。
  • 我是这样看的。有两种方法可以创建订单。一个经历:介绍号码,产品和其他东西。一屏又一屏。游览完成后,将显示摘要屏幕,用户可以在其中修改和确认。或者您可以从头开始在此摘要屏幕上创建它。您也可以从任何其他游览屏幕转到此摘要屏幕。如果用户在游览中只选择了数字和产品,这些将是出现在摘要中的那些,而不是其他的,...
  • ...他们可以在摘要中完成。
  • 我没有看到将订单保存为“不完整”状态的大问题,稍后将更改为其他内容。顺便说一句,您的 UI 应该通过命令与您的域对话,如果您在尚未完成时不从您的 UI 存储聚合,那么在那一刻您应该使用聚合,但也许您可以准备一个稍后将执行的 CreateOrder 命令创建聚合
  • 如果订单因其他业务原因被保存为不完整状态,则存在问题。在其他应用程序中必须阅读所有订单,并且这些订单必须处于完整状态。最后,我创建了另一个名为 OrderInProgress 的聚合(出于某些原因,我更喜欢创建一个属性“IsInProgress”或一个可枚举的“状态”)和一个将其保存在内存中的存储库。我不知道是否是更好的解决方案,但不幸的是,我的演示时间已经不多了:(
猜你喜欢
  • 2014-08-11
  • 1970-01-01
  • 1970-01-01
  • 2022-12-16
  • 2016-11-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-03-20
相关资源
最近更新 更多