【问题标题】:Separating business rules from entities in domain driven design在领域驱动设计中将业务规则与实体分离
【发布时间】:2018-01-21 09:20:10
【问题描述】:

当我在我的软件项目中实践 DDD 时,我一直面临这样一个问题:“为什么我应该在实体中实现我的业务规则?它们不应该是纯数据模型吗?”

请注意,根据我对 DDD 的理解,域模型可以由持久模型和值对象组成。

我想出了一个解决方案,将我的持久模型与我的域模型分开。另一方面,我们有数据传输对象(DTO),所以我们有 3 层数据映射。数据库到持久性模型,持久性模型到域模型和域模型到 DTO。在我看来,我的解决方案不是一个有效的解决方案,因为必须付出太多的努力。

那么有没有更好的实践来实现这个目标?

【问题讨论】:

  • 太多分层/映射的问题已经在 SO 上讨论了数百次。它也与“在实体中实现业务规则”完全无关。
  • @guillaume31 你能提供一些链接吗?也许这些可以帮助 Behzad...
  • @MarkSeemann 你有this one吗?
  • @guillaume31 我没有想到,虽然我同意你的观点,这是一个基本上在这里完成的话题,我们不能指望新手知道这一点......我不是钓鱼链接到我的博客,但谢谢:)
  • @guillaume31 我知道对象-对象映射器/分层,我的问题不是关于它。我已经在 SO 中搜索了几次,但我找不到我的答案,但如果我犯了错误,我会感谢你的帮助。无论如何,完全不同的问题可能有相同的答案,这对社区从不同的角度获得答案是有好处的。

标签: domain-driven-design software-design


【解决方案1】:

免责声明:这个答案比问题大一点,但需要理解问题;根据我的经验,也是 100%。

你的感觉很正常,我前段时间也有同样的感觉。这是因为架构、编程语言和使用的框架的组合。您应该尝试选择上述工具,以便它们提供最容易更改的代码。如果您必须为添加到实体的每个字段更改 3 个类,那么这对于大型项目(即 50 多种实体类型)来说将是一场噩梦。

问题是每个实体/概念有多个 DTO。

我使用的最重的架构是经典的分层架构;严格版本是最难的(在严格版本中,一个层只能访问它之前的层;即用户界面只能访问应用程序)。随着数据从基础架构转移到 UI,它涉及大量 DTO 和翻译。测试也很困难,因为我不得不使用很多模拟。

然后我倒置依赖,Domain 将不依赖于 Infrastructure。为此,我在基础设施中实现的领域层定义了接口。但我仍然需要对他们使用嘲笑。此外,Aggregates 不是纯粹的,它们有副作用(因为它们称为 Infrastructure,即使它是由接口抽象出来的)。

然后我将域移到了最底部。这使我的聚合变得纯净。我不再需要使用模拟。但我仍然需要 DTO(由 Application 层返回给 UI 以及由 ORM 使用的 DTO)。

然后我实现了第一个飞跃:CQRS。这将模型分为两部分:写入模型和读取模型。重要的是您不再需要对模型使用 DTO。聚合(写入模型)可以按原样序列化或转换为 JSON 并存储在几乎任何数据库中。沃恩弗农有一个blog post about this

但最好的是 Read 模型。您可以为每个用例创建一个读取模型。作为仅用于读取/查询的模型,它可以尽可能简单/转储。读取的实体仅包含与查询相关的行为。通过正确的持久性,它们可以保持原样。例如,如果您使用MongoDB(或任何文档数据库),通过简单的基于反射的序列化程序,您可以拥有非常精简的架构。由于域事件,您不需要使用 JOINS,您可以进行完整的数据非规范化(读取的实体包括他们需要的所有数据)。

第二个飞跃是事件溯源。有了这个,您不需要聚合的平坦持久性。每次处理命令时,它们都会从事件存储中重新水化。

您仍有 DTO(命令、事件、读取模型),但每个实体/概念只有一个 DTO。

关于 Presentation 使用的 DTO 的消除:您可以使用 GraphSQL 之类的东西。

编程语言和框架会使上述所有情况变得更糟。强类型编程语言强制您为每个自定义返回值创建一个类型。某些框架会强制您返回自定义的可序列化类型,以便通过 HTTP 请求将它们返回到 REST(通过这种方式,您可以使用反射获得自我描述的 REST 端点)。在 PHP 中,您可以简单地使用带有字符串键的数组作为 REST 控制器返回的值。

附:

  • 我所说的 DTO 是指一个有数据但没有行为的类。
  • 我并不是说我们都应该使用 CQRS,只是你应该知道它的存在。

【讨论】:

  • 你已经回答了我的问题。但还有一件事,为了隔离和安全,暴露我的聚合模型听起来很可疑。还是我错过了什么?
  • @Behzad 在哪种架构中以及出于什么目的:读还是写?
  • 用于 CQRS 中的写入目的。我的理解是我们有不同的命令,它们负责在不使用 DTO 的情况下更改每个聚合的状态,这意味着按原样公开数据。
  • @Behzad 命令只是 DTO(除了原始验证之外没有其他行为的结构)。它们被发送到命令处理程序(应用程序服务),该处理程序从持久性加载聚合(在事件来源的情况下:从事件存储),它调用聚合上的适当方法,它收集生成的事件(其他 DTO)然后它持久化聚合和事件(如果是事件源,它只保留事件)。因此,聚合仅由命令处理程序公开(=使用)。
  • @Behzad 我使用另一种风格,cqrs.nu 推广的风格,其中命令直接到达聚合;命令处理程序非常通用。我喜欢这种风格,因为它将应用程序命令处理程序的数量减少到一个,只需一点约定:聚合的命令方法的格式为handleXXX,其中 XXX 是命令的简称。在我最新的项目中(一个自定义的 CRM 有大约 100 个实体类型)我几乎有零类重复。想象一下拥有 N x 100 个课程而不是 100 个课程意味着什么。
【解决方案2】:

为什么要在实体中实施我的业务规则?它们不应该是纯数据模型吗?

您的持久性实体应该是纯数据模型。您的 实体描述行为。它们不是一回事。在存储库中使用一些逻辑来更改另一个是一种常见的模式。

我所知道的最简洁的管理方式是将持久性实体视为由域实体管理的 值对象,并使用 data mapper 之类的东西在域之间进行转换和坚持。

另一方面,我们有数据传输对象 (DTO),因此我们有 3 层数据映射。数据库到持久性模型,持久性模型到域模型和域模型到 DTO。在我看来,我的解决方案不是一个有效的解决方案,因为必须付出太多的努力。

在这里提供了一些简化,基于这样的想法,即如果您正在实施查询,您实际上并不需要“域模型”,因为您实际上不会更改支持数据。在这种情况下,您可以完全摆脱“领域模型”的循环。

【讨论】:

    【解决方案3】:

    DDD 和数据是非常不同的东西。聚合的数据(结果)将以某种方式保存,具体取决于您使用的内容。我个人认为在域事件中,因此生成的域事件是 DTO(技术上它是),可以直接存储在事件存储中(如果您使用事件源)或充当持久性模型的数据源。

    域模型表示相关域行为,域状态是“结果”。与仅表示业务语义值的值对象相比,实体是具有 id 的概念。一个实体通常对相关的值对象和一致性规则进行分组。 Not all business rules are here , some of them make sense as a service.

    现在,在 CRUD 域或 CRUD 建模的情况下,基本上您所拥有的只是一些数据结构和一些验证规则。如果建模正确,则无需在这里使您的生活复杂化。尽可能简单地实现事情。

    始终将 DDD 视为一种收集需求和构建信息的方法。代码(设计)中的实现是不同的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-10-26
      • 1970-01-01
      • 2011-08-01
      • 1970-01-01
      • 2015-04-22
      相关资源
      最近更新 更多