实现:构建基块

这是本系列的重要组成部分。我们将用实例介绍和解释一些明确的规则。您可以遵循这些规则,并在实施域驱动设计时应用到您的解决方案中。

示例领域

示例将使用 GitHub 使用的一些概念,如问题、存储库、标签和用户,您已经熟悉。

下图显示了一些聚合、聚合根、实体、价值对象及其之间的关系:

图像

问题聚合包括包含注释和问题标签集合的问题聚合根。

其他聚合显示为简单,因为我们将专注于问题聚合:

图像

集合

如前所称,聚合是由聚合根对象结合在一起的对象(实体和值对象)集合。

聚合/聚合根原理

业务规则

实体负责执行与其自身属性相关的业务规则。聚合根实体还负责其子收集实体。

聚合体应通过实施域规则和约束来保持其完整性和有效性。

这意味着,与 DTO 不同,实体有实现某些业务逻辑的方法。实际上,我们应该尽可能在实体中实施业务规则。

单单元

聚合作为单个单元检索和保存,并具有所有子集合和属性。例如,如果您要向问题添加注释,则需要添加注释。

  • 从数据库中获取问题,包括所有子集合(评论和问题实验室)。
  • 使用问题类上的方法添加新注释,如问题。
  • 将问题(所有子集合)作为单个数据库操作(更新)保存到数据库。

对于以前与 EF 核心和关系数据库合作的开发人员来说,这似乎很奇怪。

获得所有细节的问题似乎没有必要和低效。我们为什么不在不查询任何数据的情况下执行到数据库的 SQL 插入命令呢?

答案是,我们应该实施业务规则 ,并维护代码中的数据一致性和完整性。

如果我们有一个业务规则,如"用户不能评论 锁定的问题",我们如何检查问题的锁定状态,而无需从数据库中检索它?

因此,只有在应用程序代码中可用的相关对象时,我们才能执行业务规则。

示例:向问题添加注释

图像

_issueRepository.GetAsync 方法默认将问题与所有 详细信息(子集合)作为单个单元检索。

虽然这对 MongoDB 来说是开箱即用的,但您需要为 EF 酷睿配置您的聚合详细信息。但是,一旦配置,存储库会自动处理它。

_issueRepository.GetAsync 方法获得一个可选参数,包括尾,您可以通过错误禁用此行为时,您需要它。

问题.AddComment 获取用户Id 和评论文本,执行必要的业务规则,并将评论添加到问题的评论集合中。

最后,我们使用_issueRepository.更新酶来保存数据库的更改 。

交易边界

聚合通常被视为交易边界。

如果一个用例与单个集合配合使用,则将其读取并保存为单个单元,则对聚合对象所做的所有更改将作为原子操作保存在一起,并且您不需要进行明确的数据库交易。

但是,在现实生活中,您可能需要在单个 使用案例中更改多个聚合实例,并且您需要使用 数据库交易来确保原子更新和数据 一致性。

序列化

聚合(与根实体和子集合)应作为单个单元在电线上进行序列化和传输。

例如,MongoDB 将聚合序列化为 JSON 文档,同时保存到数据库,并在从数据库中读取时从 JSON 中分离。

以下规则已经带来了序列化。

聚合 / 聚合根规则和最佳实践

以下规则确保执行上述原则 。

仅通过 ID 引用其他聚合体

第一条规则说聚合应该仅通过其 Id 引用其他聚合。这意味着您不能将导航属性添加到其他聚合体中。

  • 此规则使实施序列化原则成为可能。
  • 它还可防止不同的聚合体相互操纵,并将聚合的业务逻辑泄漏到彼此之间。

在下面的示例中,您看到两个聚合根,Git 存储和问题:

图像

  • Git 存储不应收集问题,因为它们是不同的聚合体。
  • 问题不应具有相关 Git 存储库的导航属性,因为它是不同的聚合体。
  • 问题可以有存储库(作为指导)。

因此,当您有问题并且需要与此问题相关的 Git 存储 时,您需要通过存储库从 数据库中明确查询它。

保持聚合体小

一个好的做法是保持一个集合简单和小。

这是因为聚合将加载和保存为一个单 一的单位和读/写一个大对象有 性能问题。请参阅下面的示例:

图像

角色聚合集具有用户轨道值对象的集合,用于跟踪分配给此角色的用户。

请注意,用户罗尔不是另一个聚合体,它不是一个问题的规则参考其他聚合仅通过 Id。

然而,这是一个实际问题。在现实生活中,角色可能会分配给数千(甚至数百万)用户,每当您从数据库中查询 Role 时,加载数千个项目(请记住:聚合由其子集合作为单个单元加载)是一个重大的性能问题。

聚合根 / 实体的主要键

  • 聚合根通常具有单个 ID 属性用于其标识符(原始标记键:PK)。我们更喜欢 Guid 作为聚合根实体的 PK。
  • 聚合中的实体(不是聚合根)可以使用复合主密钥。

图像

  • 组织具有 Guid 标识符 (Id)。
  • 组织用户是组织的子集合,具有由组织Id和用户Id组成的复合主密钥。

聚合根 / 实体的构造器

构造器位于实体生命周期开始的位置。设计精良的构造者负有一些责任:

  • 获取所需的实体属性作为创建有效实体的参数。应强制仅通过所需的参数,并可能获得非必需属性作为可选参数。
  • 检查参数的有效性。
  • 初始化子集合。

图像

  • 通过在其构造器中获取最小要求属性作为参数,正确发布类强制创建有效实体。
  • 构造器验证输入(如果给定值是空的,请检查。NotNullOrWhiteSpace(…) 抛出参数例外)。
  • 它初始化了子集合,因此在创建问题后尝试使用标签集合时,不会遇到空引用例外。
  • 构造器还取取 ID 并传递给 基础类。我们不会在构造器内部生成 Guids,以便能够将此责任委托给其他服务。
  • 私人空构造器对于 ORM 是必要的。我们 将其保密,以防止意外地使用它在我们自己的 代码。

实体属性访问器和方法

上面的例子可能看起来很奇怪!例如,我们强制在构造器中传递非空标题。

但是,开发商可以在没有任何控制的情况下将标题属性设置为无效。这是因为上面的示例代码只关注构造器。

如果我们向公共设置者申报所有属性(如上文的示例问题类),则不能强制实体在其生命周期中的有效性和完整性。

所以:

  • 当您在设置该属性时需要执行任何逻辑时,请使用私人设置器进行属性设置。
  • 定义操作此类属性的公共方法。

示例:以受控 方式更改属性的方法

图像

  • 存储库设置器是私密的,在创建问题后无法更改它,因为这是我们在这个领域想要的:问题不能移动到另一个存储库。
  • 文本和分配使用者具有公共设置器,因为对它们没有限制。它们可以是空的或任何其他值。我们认为没有必要定义单独的方法来设置它们。如果以后需要,我们可以添加方法,使设置器保密。域层的中断更改不是问题,因为域层是一个内部项目,它不会暴露给客户端。
  • 关闭和发行关闭是对属性。定义关闭和重新开机的方法,以一起更改它们。这样,我们就无法无缘无故地解决问题。

业务逻辑与实体中的例外情况

当您在实体中实施验证和业务逻辑时,您经常需要管理特殊案例。

  • 创建域特定例外。
  • 必要时将这些例外情况放入实体方法中。

图像

这里有两个业务规则:

  • 锁定的问题无法重新打开。
  • 您无法锁定未结问题。

在这些情况下,问题类会抛出问题状态例外,以 强制执行业务规则:

图像

抛出这种例外有两个潜在的问题:

  1. 如果出现此类异常,最终用户应看到异常(错误)消息吗?如果是,您如何定位异常消息?您不能使用本地化系统,因为不能在实体中注入和使用 IString 本地化器。
  2. 对于 Web 应用程序或 HTTP API,HTTP 状态代码应返回给客户端?

ABP 的例外处理系统解决了这些和类似的问题。

示例:抛出代码的业务例外

图像

  • 问题状态例外类继承了业务例外类。ABP 默认返回 403(禁止)HTTP 状态代码(而不是 500 - 内部服务器错误),但从业务例外中得出的例外情况。
  • 该代码用作本地化资源文件 中的密钥,用于查找本地化消息。

现在,我们可以更改以下重新开机方法:

图像

并在本地化资源中添加一个条目,如下所示:

图像

  • 抛出异常时,ABP 会自动使用此本地化消息(基于当前语言)显示给最终用户。
  • 例外代码(问题跟踪:此处无法启动锁定问题)也发送给客户端,因此它可以按程序处理错误案例。