在领域驱动设计中将业务规则与实体分离
Separating business rules from entities in domain driven design
当我在我的软件项目中实践 DDD 时,我一直面临 "Why should i implement my business rules in the entities? aren't they supposed to be pure data models?"
的问题
请注意,根据我对 DDD 的理解,域模型可以由持久模型和值对象组成。
我想出了一个解决方案,将持久模型与领域模型分开。另一方面,我们有数据传输对象 (DTO),因此我们有 3 层数据映射。数据库到持久性模型,持久性模型到域模型和域模型到 DTO。在我看来,我的解决方案不是一个有效的解决方案,因为必须付出太多的努力。
那么有什么更好的做法可以实现这个目标吗?
Why should i implement my business rules in the entities? aren't they supposed to be pure data models?
您的持久性实体应该是纯数据模型。您的 域 实体描述了行为。它们不是一回事;在存储库中使用一些逻辑将一个更改为另一个是一种常见的模式。
我所知道的最简洁的管理方式是将持久化实体作为一个 值对象 由域实体管理,并使用类似 data mapper 用于域和持久性之间的转换。
On the other hand we have data transfer objects (DTO), so we have 3 layers of data mapping. Database to persistence model, persistence model to domain models and domain models to DTOs. In my opinion, my solution is not an efficient one as too much hard effort must be put into it.
cqrs 在这里提供了一些简化,基于这样的想法,如果你正在实现一个查询,你并不真的需要 "domain model" 因为你实际上不会改变支持数据。在这种情况下,您可以将 "domain model" 完全从循环中取出。
DDD 和数据是非常不同的东西。聚合的数据(结果)将根据您使用的内容以某种方式保留。我个人认为在域事件中,因此生成的域事件是 DTO(技术上是),可以直接存储在事件存储中(如果您使用事件溯源)或充当持久性模型的数据源。
域模型表示相关域行为,域状态为'result'。与仅表示业务语义值的值对象相比,实体是具有 id 的概念。实体通常对相关的值对象和一致性规则进行分组。 Not all business rules are here , some of them make sense as a service.
现在,在 CRUD 域或 CRUD 建模的情况下,基本上您拥有的只是一些数据结构和一些验证规则。如果建模是正确的,则无需在这里使您的生活复杂化。尽可能简单地实现事情。
始终 将 DDD 视为一种收集需求和构建信息的方法。代码(设计)中的实现有所不同。
免责声明:这个答案比问题大一点,但需要理解问题;根据我的经验,也是 100%。
你的感觉很正常,我前段时间也有同样的感觉。这是因为体系结构、编程语言和使用的框架的组合。您应该尝试选择上述工具,以便它们提供最容易更改的代码。如果您必须为添加到实体的每个字段更改 3 classes,那么这在大型项目(即 50 多种实体类型)中将是一场噩梦。
问题是每个 entity/concept 有多个 DTO。
我使用的最重的架构是Classic分层架构;严格版本是最难的(在严格版本中,一个层只能访问它之前的层;即用户界面只能访问应用程序)。当数据从基础架构移动到 UI 时,它涉及大量 DTO 和翻译。测试也很困难,因为我不得不使用大量模拟。
然后我倒转了依赖,Domain不会依赖Infrastructure。为此,我在基础架构中实现的域层中定义了接口。但我仍然需要为他们使用模拟。另外,Aggregates 不是纯粹的,它们有副作用(因为它们称为 Infrastructure,即使它被接口抽象)。
然后我把域移到了最底部。这使我的聚合纯净。我不再需要使用模拟。但我仍然需要 DTO(return 由应用层编辑到 UI 和 ORM 使用的那些)。
然后我迈出了第一步:CQRS。这将模型分为两部分:写入模型和读取模型。重要的是您不再需要为模型使用 DTO。聚合(写入模型)可以按原样序列化或转换为 JSON 并存储在几乎任何数据库中。沃恩弗农有 blog post about this。
但最好的是阅读模型。您可以为每个用例创建一个读取模型。作为仅用于read/query的模型,它可以尽可能simple/dump。读取的实体只包含查询相关的行为。通过正确的持久化,它们可以保持原样。例如,如果您使用 MongoDB
(或任何文档数据库),使用一个简单的基于反射的序列化程序,您可以拥有一个非常薄的架构。多亏了领域事件,你将不需要使用 JOINS,你可以拥有完整的数据反规范化(读取的实体包括他们需要的所有数据)。
第二个飞跃是事件溯源。有了这个,您不需要聚合的平坦持久性。每次处理命令时,它们都会从事件存储中重新水化。
您仍有 DTO(命令、事件、读取模型),但每个 entity/concept.
只有一个 DTO
关于删除演示文稿使用的 DTO:您可以使用类似 GraphSQL.
的内容
编程语言和框架可以使上述所有情况变得更糟。强类型编程语言强制您为每个自定义 returned 值创建一个类型。某些框架强制您 return 自定义可序列化类型,以便 return 它们通过 HTTP 请求进行 REST(这样您就可以使用反射来拥有自我描述的 REST 端点)。在 PHP
中,您可以简单地使用带有字符串键的数组作为由 REST 控制器 return 编辑的值。
P.S。
- 我所说的 DTO 是指 class 有数据但没有行为。
- 我并不是说我们都应该使用 CQRS,只是说您应该知道它的存在。
当我在我的软件项目中实践 DDD 时,我一直面临 "Why should i implement my business rules in the entities? aren't they supposed to be pure data models?"
的问题请注意,根据我对 DDD 的理解,域模型可以由持久模型和值对象组成。
我想出了一个解决方案,将持久模型与领域模型分开。另一方面,我们有数据传输对象 (DTO),因此我们有 3 层数据映射。数据库到持久性模型,持久性模型到域模型和域模型到 DTO。在我看来,我的解决方案不是一个有效的解决方案,因为必须付出太多的努力。
那么有什么更好的做法可以实现这个目标吗?
Why should i implement my business rules in the entities? aren't they supposed to be pure data models?
您的持久性实体应该是纯数据模型。您的 域 实体描述了行为。它们不是一回事;在存储库中使用一些逻辑将一个更改为另一个是一种常见的模式。
我所知道的最简洁的管理方式是将持久化实体作为一个 值对象 由域实体管理,并使用类似 data mapper 用于域和持久性之间的转换。
On the other hand we have data transfer objects (DTO), so we have 3 layers of data mapping. Database to persistence model, persistence model to domain models and domain models to DTOs. In my opinion, my solution is not an efficient one as too much hard effort must be put into it.
cqrs 在这里提供了一些简化,基于这样的想法,如果你正在实现一个查询,你并不真的需要 "domain model" 因为你实际上不会改变支持数据。在这种情况下,您可以将 "domain model" 完全从循环中取出。
DDD 和数据是非常不同的东西。聚合的数据(结果)将根据您使用的内容以某种方式保留。我个人认为在域事件中,因此生成的域事件是 DTO(技术上是),可以直接存储在事件存储中(如果您使用事件溯源)或充当持久性模型的数据源。
域模型表示相关域行为,域状态为'result'。与仅表示业务语义值的值对象相比,实体是具有 id 的概念。实体通常对相关的值对象和一致性规则进行分组。 Not all business rules are here , some of them make sense as a service.
现在,在 CRUD 域或 CRUD 建模的情况下,基本上您拥有的只是一些数据结构和一些验证规则。如果建模是正确的,则无需在这里使您的生活复杂化。尽可能简单地实现事情。
始终 将 DDD 视为一种收集需求和构建信息的方法。代码(设计)中的实现有所不同。
免责声明:这个答案比问题大一点,但需要理解问题;根据我的经验,也是 100%。
你的感觉很正常,我前段时间也有同样的感觉。这是因为体系结构、编程语言和使用的框架的组合。您应该尝试选择上述工具,以便它们提供最容易更改的代码。如果您必须为添加到实体的每个字段更改 3 classes,那么这在大型项目(即 50 多种实体类型)中将是一场噩梦。
问题是每个 entity/concept 有多个 DTO。
我使用的最重的架构是Classic分层架构;严格版本是最难的(在严格版本中,一个层只能访问它之前的层;即用户界面只能访问应用程序)。当数据从基础架构移动到 UI 时,它涉及大量 DTO 和翻译。测试也很困难,因为我不得不使用大量模拟。
然后我倒转了依赖,Domain不会依赖Infrastructure。为此,我在基础架构中实现的域层中定义了接口。但我仍然需要为他们使用模拟。另外,Aggregates 不是纯粹的,它们有副作用(因为它们称为 Infrastructure,即使它被接口抽象)。
然后我把域移到了最底部。这使我的聚合纯净。我不再需要使用模拟。但我仍然需要 DTO(return 由应用层编辑到 UI 和 ORM 使用的那些)。
然后我迈出了第一步:CQRS。这将模型分为两部分:写入模型和读取模型。重要的是您不再需要为模型使用 DTO。聚合(写入模型)可以按原样序列化或转换为 JSON 并存储在几乎任何数据库中。沃恩弗农有 blog post about this。
但最好的是阅读模型。您可以为每个用例创建一个读取模型。作为仅用于read/query的模型,它可以尽可能simple/dump。读取的实体只包含查询相关的行为。通过正确的持久化,它们可以保持原样。例如,如果您使用 MongoDB
(或任何文档数据库),使用一个简单的基于反射的序列化程序,您可以拥有一个非常薄的架构。多亏了领域事件,你将不需要使用 JOINS,你可以拥有完整的数据反规范化(读取的实体包括他们需要的所有数据)。
第二个飞跃是事件溯源。有了这个,您不需要聚合的平坦持久性。每次处理命令时,它们都会从事件存储中重新水化。
您仍有 DTO(命令、事件、读取模型),但每个 entity/concept.
只有一个 DTO关于删除演示文稿使用的 DTO:您可以使用类似 GraphSQL.
的内容编程语言和框架可以使上述所有情况变得更糟。强类型编程语言强制您为每个自定义 returned 值创建一个类型。某些框架强制您 return 自定义可序列化类型,以便 return 它们通过 HTTP 请求进行 REST(这样您就可以使用反射来拥有自我描述的 REST 端点)。在 PHP
中,您可以简单地使用带有字符串键的数组作为由 REST 控制器 return 编辑的值。
P.S。
- 我所说的 DTO 是指 class 有数据但没有行为。
- 我并不是说我们都应该使用 CQRS,只是说您应该知道它的存在。