【问题标题】:Where does my DDD logic belong?我的 DDD 逻辑属于哪里?
【发布时间】:2026-01-10 01:25:01
【问题描述】:

我被 Eric Evans 的书说服了,并且正在将 DDD 集成到我的框架中。所有基本元素(服务、存储库、限界上下文等)都已实现,现在我正在寻找有关如何正确集成这些元素的反馈。

我有一些业务逻辑必须在创建或修改实体时执行。这个例子是一个非常简单的例子。大多数业务逻辑将变得更加复杂。

这个业务逻辑可以分解为以下动作:

  1. 更新计算字段;
  2. 更新聚合根内的子记录。在创建聚合根时,这需要创建默认子记录。更新聚合根时,如果聚合根上的特定字段发生更改,则需要删除现有子记录并创建新记录;
  3. 将聚合根的开始和结束日期传播到聚合根内子记录的开始和结束日期。在某些情况下,这些必须保持同步;
  4. 将聚合根的字段传播到不同的聚合根。

我的第一次尝试是将所有这些都放在聚合根上,但我觉得这行不通。我在集成此逻辑时遇到以下问题:

  • 所有这些操作必须作为一个整体完成,不应作为单独的操作提供。这导致测试非常困难 (TDD);
  • 我不清楚这些操作是否可以移出到服务中。这样做的原因是它们在聚合根之外毫无意义,但它会使 TDD 更容易;
  • 根据是创建新实体还是修改现有实体,某些逻辑会发生变化。我应该将这两个分支放在更新逻辑中,还是应该创建两个完全不同的路径来共享不区分基于创建/修改的业务代码。

对于上述问题的任何帮助以及其他一般反馈将不胜感激。

【问题讨论】:

  • 你真的要向孩子们传播价值观吗?即你不能只对从父级读取值的子级使用 getX() 方法吗?例如Child.getX() {返回 parent.getX();}。甚至可能使您不必更换孩子。只是一个想法。
  • 旧版应用程序。你要做什么:)。
  • wrt @Kdeveloper 对服务的评论。 DDD 确实在谈论服务。风险在于所有业务逻辑最终都在服务中,将对象仅作为数据容器(下面提到的“贫血数据模型@orangepips”)。因此,DDD 建议基本上是:首先,询问任何域行为属于哪个对象。如果它真的不属于单个对象,那么 - 并且只有这样 - 将它放入域服务中。
  • 我还想引用 Evans DDD,关于服务的章节(第 104 页):“有一些重要的域操作无法在实体或值对象中找到自然归宿。其中一些本质上是活动或动作,而不是事物,但由于我们的建模范例是对象,所以无论如何我们都会尝试将它们拟合到对象中。现在,更常见的错误是放弃将行为拟合到适当的对象中,逐渐滑向程序化编程。”
  • 非常同意以上所有。只有最后的评论是要警惕您对“服务层”一词的看法。将其可视化为“上方”的一层或提供对域实体/值的访问是有危险的。这反过来又会滋生一种误解,即其他层(例如应用层)只能调用服务,而不能直接调用域实体/值上的操作。这是不对的 - 并且可能导致贫血的域模型。最好将“域层”视为由具有同等地位的域实体、值和服务组成。

标签: oop design-patterns tdd domain-driven-design


【解决方案1】:

您描述的算法应该保留在聚合根中,否则您最终会得到anemic domain model,除了将字段传播到另一个聚合根,我将在此描述我认为您稍后应该做什么。

就 TDD 而言,在聚合根上具有“包”访问权限的方法(例如“calculate()”)应该协调整个操作,服务或存储库对象通常会调用该操作。这就是测试应该与设置不同的实例变量组合一起练习。聚合根应该公开它的实例变量,孩子集合,每个孩子都应该通过getter公开它的实例变量——这允许测试验证它们的状态。在所有情况下,如果你需要隐藏信息使这些 getter 封装或私有访问,并使用您的单元测试框架将它们公开以进行测试。

对于您的测试环境,请考虑使用 mocking 存储库对象(您使用的是 dependency injection 对吗?)以返回硬编码值。除此之外,请考虑使用dbunit 之类的东西来处理处于已知状态的数据库。

就逻辑更改而言,创建与修改,您是指如何持久化还是需要考虑实际的算法?如果是前者,我会让存储库负责,如果是后者,我会创建两个单独的方法(例如“calculateCreate()”和“calculateUpdate()”),calculate() 将酌情委托。

此外,还需要考虑一个并发问题,因为听起来计算值似乎依赖于可变字段。因此,要么需要仔细锁定,要么需要聚合根,客户端一次只能使用一次。这也适用于跨聚合传播字段 - 我可能会为此目的使用存储库 - 但您需要仔细考虑这应该或不应该如何影响正在使用存储库对象的其他客户端。

【讨论】:

  • 感谢您的回复。首先,是的,我正在使用 Castle Windsor 和 Rhino。此外,您是说我应该在包级别为 TDD 提供单独的方法?
  • 是的。通过让单元测试包为测试目的公开该方法,使单元测试可以直接调用包访问方法。对于您的应用程序,这些方法被隐式调用 - 因此使用 aggregateRoot 的代码将无法直接调用 calculate() 因为它不是公共的。所以改为 service.save(aggregateRoot) - 或 repository.save(aggregateRoot) - 在内部调用 aggregateRoot.calculate()。
  • 关于创建/更新逻辑。决定调用哪些业务逻辑方法的方法应该根据创建/更新模式来区分,而不是业务逻辑本身?
  • 在持久性方面,如果可能的话,我会使用相同的路径进行创建和更新,例如“服务。保存()”。在服务内部,我会做你最容易进行单元测试的事情。根据您所写的内容,我认为您可以检查是否存在 aggregateRoot 并酌情更新或插入。对于孩子来说,听起来他们的存在似乎依赖于父母,在这种情况下,最简单的方法可能总是删除然后插入。当然,如果有很多孩子,这可能会证明效率太低,但它绝对是最直接的。
  • 我正在使用 CQRS,所以实体本身决定执行什么业务逻辑;非常巧妙的想法。我想我理解这个想法。非常感谢您的帮助。