DDD - 使用流程管理器或领域服务

4
我刚开始学习DDD,并在应用程序的一部分实现它,因为应用程序的某些要求导致我使用CQRS和事件溯源(需要记录系统中发生的事件的历史记录以及需要能够查看过去系统的状态)。
阅读Vaughn Vernon的书籍和其Effective Aggregate Design系列后,我有一个问题是:Long Running Process(流程管理器)和Domain Service(领域服务)之间有什么区别,特别是当您将导航属性指向另一个聚合时。
我理解的是:
- 领域服务用于保存不属于任何聚合的逻辑。根据Vaughn的说法,它也可以用于将实体引用传递到包含它的聚合中。它还可以用于管理事务,因为无法在域对象中处理它们。
- 流程管理器用于编排对系统进行的修改,并跨不同的聚合范围。有些人说,一个好的流程管理器实际上是一个聚合根。从我的理解来看,它不会管理事务,因为事件是在提交更改后启动的。它采用最终一致性的方法。最终所有的更改都将发生。

现在,为了将所有内容放入上下文中。我正在构建的应用程序的核心是处理包含其自己逻辑的节点树。我们需要能够向树中添加节点,当然也需要创建这些节点。
我们需要能够知道这些节点发生了什么。即,我们需要能够检索与节点相关联的事件。
还有一种对叶子节点进行的修改(取决于修改的类型)必须被复制到此节点的父节点中的其他节点。

我的聚合是什么:
- Nodes,它是我的树所包含的内容。在我看来,这是一个聚合,原因有几个。它不是不变量,因此不是值对象。它们具有自己的领域逻辑,允许它们为其属性分配值对象,并且我们需要能够使用ID访问它们。
- 由节点组成的非二叉树的表示。现在,我实际上将其设计为我的聚合根,它实际上是一个流程管理器。该树包含此树的逻辑表示。它包含树的根。这个根实际上是一个对象(我不确定它是否可以称为值对象,因为它包含对其他聚合的引用,子节点,但它肯定听起来像是)。树中的节点对象包含基本信息,如节点名称和指向实际聚合的引用(这几乎听起来像两个有界上下文?)
使用这种方法,以下是正在发生的事情:
- 执行创建Node的命令后,会创建并提交一个Node。NodeCreated事件被触发,被正确的处理器捕获,该处理器检索与此节点关联的Tree(进程管理器)并将节点添加到正确的位置(使用Node的父ID属性)
- 执行修改Node的命令后,节点被修改并提交。NodeModified事件被触发,被处理器捕获。然后,处理器检索Tree(我的进程管理器)并查找所有修改的节点的父节点,并请求这些节点根据Child Node上的修改来修改它们自己的属性。所有这些都很有道理,对我来说看起来几乎很漂亮,展示了事件的力量和域逻辑的分离

但是,我的主要问题在于事务。如果在更新Tree和需要修改或添加的节点时出现错误会发生什么?因为已经提交了Node的事件,所以必须创建新的事件来恢复修改吗?我知道输入系统时命令必须有效,因此这不是验证问题,而且出现问题的可能性就像一百万分之一。这是否意味着我们不应考虑这种可能性?

事务问题正是我感觉应该使用服务的原因。无论是应用程序服务(此处为命令处理程序)还是域服务,都可以进行编排修改并在单个事务中完成。如果在此过程中出现故障,则不会创建/修改任何内容,但这违反了DDD规则,即我不应在同一交易中修改多个聚合。这看起来有些不太优雅的解决方案

我真的觉得自己在这方面缺少了什么,但我不确定究竟是什么。

2个回答

1
有些人认为一个好的进程管理器实际上是一个聚合根。
从我的角度来看,这是不正确的。一个进程管理器或者Saga协调跨多个聚合实例的长时间运行的业务流程。它最终将系统带入有效的最终状态。它不会发出事件,但会响应事件并创建命令,这些命令到达聚合(可能通过命令处理程序,具体取决于您的架构)。那些说这话的架构师未能正确识别聚合边界。
进程管理器/Saga可以是有状态的 - 但只是为了记住它所做的进展;它可以有一个进程ID;甚至可以是事件源。
进程管理器旨在协调在系统上进行的修改,并涵盖不同的聚合。
是的,这是正确的。
执行修改节点的命令后,节点被修改并提交。
当设计聚合时,您必须考虑保护写/命令端架构中存在的不变量和业务规则;这是产生状态转换,在事件驱动架构中发出事件的一侧。

在您的特定情况下,我确定的单个业务规则(如果有)是:当节点被创建(类似于CRUD操作!)时,会发出NodeCreated事件;类似于NodeModified。因此,这些操作存在于写/命令方面。

NodeModified事件被启动,由处理程序捕获。然后,处理程序检索树(我的过程管理器),找到修改节点的所有父节点,并要求这些节点根据子节点的修改内容修改自己的属性。

关于更新父节点的写入方面是否有任何业务规则?我没有看到任何规定。当然,在创建节点后会更新某些内容,但它不是聚合而是读取模型。实际上被调用的处理程序是一个读取模型。它将NodeXXX事件投影到一组节点的树中。


@Guigui 是的,但是这些新值(存储在父节点上)在父节点处理未来命令时使用吗?还是只用于显示给用户? - Constantin Galbenu
@Guigui 但这是查询/读取类型的逻辑,而不是聚合使用的写入/命令类型的逻辑。 - Constantin Galbenu
@Guigui 是的,确实是领域逻辑但是查询端。这意味着没有Saga,只有一个读模型。读模型与Saga不同。读模型可以在任何时候重新创建,而Saga则不能。Saga发送命令,读模型不会等等。正确识别组件类型非常重要。 - Constantin Galbenu
听起来这就是我缺失的部分。在我看来,读模型没有任何逻辑。但你所说的有道理。让我多了解一下。 - Guigui
@Guigui 很高兴能帮忙。阅读模型非常重要,也是用户看到的方面。仅关于阅读模型就可以写10本大书。 - Constantin Galbenu
显示剩余5条评论

0
我真的觉得我在这里缺少了什么,但我不确定是什么。
您可能过于复杂化了您的领域模型。
领域服务通常是提供领域模型访问(缓存)状态或能力的“服务提供者”。例如,我们可以使用领域服务来使模型访问缓存的税表,以便它可以计算订单上的税费;或者我们可以使用领域服务来使模型访问委托给电子邮件基础设施的notifyCustomer功能。
流程管理器通常用于编排-它们基本上是查看已发生的事情(事件)并建议运行其他命令的状态机。请参见 Rinat Abdullin 的描述。
如果在更新树和必须修改或添加的节点时发生错误会发生什么?节点的事件已保存在事件存储中,因为它已提交。所以我需要创建一个新的事件来撤消修改吗?
也许-补偿事件是一种常见的模式。
重点是:在跨多个事务进行变更的编排中没有什么神奇的地方。考虑一下如何安排一个 UI,向人类操作员显示正在发生的情况、接下来应该发生什么以及故障模式是什么。
机会发生某些事情的可能性就像一百万分之一。这是否意味着我们不应该考虑这种可能性?
这取决于业务风险。但正如 Greg Young 在他的演讲 Stop Over Engineering 中指出的那样,如果您可以将那 100 万分之一的问题升级到人类处理,您可能已经做得足够了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接