【问题标题】:How to model entity composed of other entities in DDD?如何在 DDD 中对由其他实体组成的实体进行建模?
【发布时间】:2017-05-18 01:50:00
【问题描述】:

让我们有一个由多个不同类型的设备(实体)组成的机器(实体)。我们在机器上有一个方法来启动它,该方法将调用构成机器的设备的 Initialize 方法。我在如何建模时遇到问题。如果我从启动的角度来看,它显然是机器的行为,它属于机器实体。但是实现需要委托给各种设备实体。我可能将机器建模为一个聚合根,并在那里有一组设备。但是每个设备也可能是它执行的行为的聚合根。至于到目前为止我发现的内容,我可以在启动方法中使用双重调度并传入设备集合。在这种情况下,调用者有责任提供这个集合。但是,既然机器实体确实知道设备是由什么组成的,为什么?将每个设备的 repo 注入机器显然是不行的。将启动方法委托给域服务也是一种选择,但这不是清楚机器的实体行为吗?还有什么想法吗?遵循 DDD 指南如何最好地对这种情况进行建模?

class Machine
{
   DeviceKey[] Devices{ get;}

   void Startup()
   {
      foreach(var dvcKey in Devices)
      {
        //how to get the entity for the given dvcKey?
        //so we can call init on it? like
        dvc.Initialize();
      }     
   }
}

更多信息:首先,感谢您回答和阅读本文。代码只是原型代码,因为问题是概念性的,如果我编写不变量、用例、事务分析等,它将成为与模式无关的巨大规范?当然领域是复杂的,我们正在用各种设备对物理机器进行建模,每个设备将执行不同的逻辑,一些逻辑,例如机器状态的验证是在所有设备上执行的(每个设备都是一个ar?)。我们有几个用例,因为一个用户将操作机器,例如启动、关闭,其他用户将使用机器(使用会话等......)域充满了实体,没有一个是愚蠢的。您将如何处理此类聚合?

我在这里添加了更多细节,尽管我觉得我们必须走一条路,吃我们做的东西。我们的第一次尝试是将机器作为聚合根,设备是其中的一部分,如下所示:

class Machine
{
  Device1Type Device1{}
  Device2Type[] Devices2{}
  void Startup()
  {
    Device1.Initialize();
    foreach(var dvc in Devices2)
    {
      dvc.Initialize();
    }
    //etc...
  }
}

但这会导致并发(一致性)问题,因为每个设备都将从机器的存储库中获取以执行其行为。此外,各种设备的状态与根(机器)保持一致,并且为了使设备执行其行为,需要加载整个机器的状态 - 也可能存在性能问题。因此,我们将远离机器的设备建模为聚合体。我们现在从问题中得到了问题。如何从一个聚合(机器)访问和委托给其他聚合(设备)而不泄漏到贫血?我想我们只会为启动操作使用域服务,因为它可能是一个过程,而不仅仅是一个操作/方法。再次感谢您的阅读。

【问题讨论】:

  • 如果没有关于领域的更多信息就无法回答:不变量、用例、事务分析等。就像现在一样,您的类只不过是愚蠢的容器,因此不建议使用 DDD。
  • 好的,我想我们将尝试将 Startup 作为一种机器方法,将 DeviceProvider 作为参数传入(如方法注入),因为它是从应用程序服务调用的。 DeviceProvider 将提供设备并且 Startup 可以调用它的 Initialize。这样机器不处理设备(加载等),启动仍然是机器的方法。
  • 我正在考虑在这里使用命令,并且处理程序是可以访问所有设备的处理程序。如果我们发现它毕竟是一个长时间运行的进程,我们可以稍后将处理程序中的实现重构为进程管理器。这样,我们也可以从域模型内部发出启动(重启)命令,而不仅仅是从应用程序中发出。服务?

标签: domain-driven-design


【解决方案1】:

您的总体界限不明确。由于我们对您的域知之甚少,我只能猜测。你说MachineDevices 组成。您将它们都称为 entities,但看起来您将它们全部聚合在 Machine 下。但是你也说设备是独立运行的,所以需要在机器启动的时候启动。

您是否清楚自己的总体边界?对我来说,设备本身就是聚合体。然后,你启动机器的命令,还包括向每个设备发送初始化命令,但这不一定是同步完成的,它看起来更像是一个长时间运行的过程,我们尝试初始化所有设备并收集响应,然后观察是否出现故障,然后更改机器的启动状态。

在您当前的方法中,Machine 承担了太多责任。如果您的foreach 中的一台设备在启动时崩溃,会发生什么情况?您的Machine 将处于哪种状态?它是有效状态还是无效状态?您将如何处理已经启动的设备?什么是一台设备并不重要,但如果它没有初始化,你会停止整个过程?你看,问题太多了,你的过程似乎过于简单了。正如我所提到的,我更倾向于将其视为独立聚合之间的编排。

查看 Udi Dahan 的这篇名为 Race Conditions Don't Exist 的文章,它可能会有所帮助。

【讨论】:

  • 感谢您的链接,我之前读过,但我会重读。你说的非常对。启动可能会成为一个长期运行的过程,但这会比此时可能需要的更复杂。所以我们首先尝试使用同步方法。是的,设备也是聚合体,它们执行仅限于它们的操作,通过它们最终更新机器状态的事件。你是对的,目前机器和设备耦合得太紧了。现在将尝试使用流程对其进行建模,以查看它是否更适合。虽然这样实体(ag)会在旅途中贫血?
  • 可能是这样,但 Udi Dahan 认为聚合可以建模为状态机/长时间运行的进程。我们在实践中这样做,它似乎工作得很好。我们确实将状态对象从状态机和状态机中分离出来,因为它们是静态类。这实际上在某种程度上倾向于函数式编程。
  • 我们现在将它建模为 machine.Startup(),它发出一个 StartupMachineCommand。命令处理程序负责启动过程。至于现在它只是加载设备并调用它们的初始化命令。如果它进一步显示,我们需要一个进程管理器,处理程序将产生一个。会接受你的回答,因为它指向我的命令处理程序方向。再次感谢。
【解决方案2】:

就像@guillaume31 在评论中所说的那样——如果不了解您的域,就不可能真正回答这个问题。但是,我有一些可能有用的提示,它们可能会为您指明正确的方向。

使用域服务

当您觉得需要执行一些不适合您的聚合之一的操作时,您应该考虑使用域服务 - DDD 构建块之一。我不能说这会解决您的所有问题 - 您最了解自己的域,因此您可以决定使用域服务是否会有所帮助。

做一些实际的建模!

如何最好地按照 DDD 指南对这种情况进行建模?

这个问题没有普遍的答案。细化代码中应该存在哪些聚合并在它们之间绘制责任边界并非易事。您应该遵循普遍存在的语言,注意聚合是否具有凝聚力,并真正考虑这些聚合应该保护的不变量。我想说一下子考虑所有这些事情不是一个人的工作——整个开发团队都应该参与建模阶段。

最强大和最有趣的技术之一是由 Alberto Brandolini 发明的 Event Storming。 Event Storming 让您可以快速探索困难的业务流程,还可以使用 DDD 进行一些建模 - 请参阅 Big Picture Event StormingDesign Level Event Storming。很少有 Event Storming 会议可以帮助您回答有关设计聚合的问题。

这些提示可能无法直接回答您的问题,但我希望能提供有用的指导。

【讨论】:

  • 感谢您提供的链接,尽管我们的领域(核心)非常知名,但我们还是领域专家 - 多年来一直致力于此领域。我们正处于重构阶段并走上了 DDD 的道路。我将使用有关我们建模的更多信息来编辑问题。我想域服务是最干净的方式,但是仅仅因为它的一部分(设备)也是聚合的,所以将机器的方法移动到服务中感觉不合适。
猜你喜欢
  • 2015-06-30
  • 2017-04-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-03
  • 1970-01-01
  • 2023-02-23
  • 1970-01-01
相关资源
最近更新 更多