需要外部服务的实体中的业务逻辑

当业务逻辑仅使用该实体的属性时,只需在实体方法中实施业务规则就很简单。

如果业务逻辑要求查询数据库或使用应从依赖性注入系统解决的任何外部服务,该怎么办?

记住:实体不能注入服务!

实现这种商业逻辑有两种常见方法:

  • 在实体方法上实现业务逻辑,并获取外部依赖作为该方法的参数。
  • 创建域服务。

稍后将解释域服务。但是,现在让我们看看如何在实体类中实现它。

示例:业务规则:不能同时向用户分配超过 3 个未发行问题

image

  • 分配的"已分配的"属性设置器为私有。因此,唯一的方法来改变它使用分配到同位素和清洁分配方法。
  • 分配到酶获得一个应用使用者实体。实际上,它只使用用户。Id, 这样你就可以得到一个 Guid 值, 像用户 Id 。但是,这种方式可确保 Guid 值是现有用户的 Id,而不是随机的 Guid 值。
  • IUserSue 服务是一种任意服务,用于为用户获取未发行计数。代码部分(称为分配 Async)有责任解决 IUser 问题服务并在此处传递。
  • 如果业务规则不符合,分配 AsingAsync 会抛出例外。
  • 最后,如果一切正确,则设置分配的"已分配的"属性"。

当您想向用户分配问题时,此方法完全保证了应用业务逻辑。但是,它存在一些问题:

  • 它使实体类取决于使实体变得复杂的外部服务。
  • 很难使用该实体。现在使用实体的代码需要注入 IUserSue 服务并传递给分配到模拟方法。

存储 库

存储库是一个类似于集合的界面,由域名和应用层用于访问数据持久性系统(数据库)来读取和编写业务对象(通常是聚合物)。

共同存储库原则是:

  • 定义域层中的存储库界面(因为它在域和应用层中使用),在基础结构层中实现(启动模板中的实体帧工作核心项目)。
  • 不在存储库中包含业务逻辑。
  • 存储库界面应是数据库提供商 / ORM 独立。例如,不要从存储库方法返回 DbSet。DbSet 是 EF 核心提供的对象。
  • 为聚合根创建存储库,而不是为所有实体创建存储库。因为,应通过聚合根访问子收集实体(聚合)。

不在存储库中包含域逻辑

虽然此规则在开始时似乎很明显,但很容易将业务逻辑泄漏到存储库中。

示例:从存储库获取非活动问题

image

IIsue 存储扩展了标准 I 存储<…通过添加"获取激活问题"方法>界面。此存储库适用于此类问题类:

让我们查看实现以了解它:

image

(用于实施 EF 核心。请参阅 EF 核心集成文档,了解如何使用 EF 核心创建自定义存储库。

当我们检查 GetInActiveSuesAsync 实施时,我们看到一条定义在活动中问题的业务规则:问题应是公开的,分配给任何人,创建于 30 多天前,在过去 30 天内不予置评。

这是隐藏在存储库方法中的业务规则的隐含定义。当我们需要重复使用此业务逻辑时,问题就会发生。

例如,假设我们要在问题实体上添加 bool IsInActive()方法。这样,当我们有问题实体时,我们可以检查活动性。

让我们查看实施情况:

image

我们必须复制/粘贴/修改代码。如果活动性的定义发生变化怎么办?我们不应忘记更新这两 个地方。这是商业逻辑的重复,这是相当危险的。

解决这个问题的一个好办法就是规范模式!

规格

规范是一个命名的、可重复使用的、可组合的和可测试的类,可根据业务规则过滤域对象。ABP 框架提供了必要的基础架构,以便轻松创建规范类,并在应用代码中使用它们。

让我们将在活动中的问题筛选器作为规范类实现:

image

规范基础类通过定义表示体简化以创建规范类。刚把表情从仓库里移了出来现在,我们可以在问题实体和 EfCore 问题存储类中重新使用不活动问题指定。

在实体中使用

规范类提供了一种 Is 满意方法,如果给定对象(实体)满足规范,则返回真实。我们可以重写问题。

image

刚刚创建了一个新的实例的不激活问题规格,并使用其 Is 满意的方法来重复使用由规范定义的表达式。

与存储库一起使用

首先,从存储库界面开始:

image

通过使用规范对象将"获取激活问题"单重命名为"简单获取问题酶"。由于规范(过滤器)已移出存储库,我们不再需要创建不同的方法来获取不同条件的问题(如 GetAssigns 问题 (…),GetLocked 问题 (…)

存储库的更新实施可以是这样的:

image

由于 ToExpress()方法返回一个表达,它可以直接传递到"在哪里"方法来过滤实体。

最后,我们可以将任何规范实例传递给 GetSuesasync 方法:

image

与默认存储库

实际上,您不必创建自定义存储库即可使用规范。

标准 I 存储库已扩展了可查询,因此您可以在它上使用标准的 LINQ 扩展方法:

image

AsyncExecuter 是 ABP 框架提供的实用工具,用于使用异步 LINQ 扩展方法(如此处的 ToListAsync),而无需依赖 EF 核心 NuGet 包。

结合规格

《规范》的一个强大方面是可组合的。

假设我们有另一个规范,只有当问题处于里程碑中时,才会返回真实:

image

此规范是参数化的,与非活动问题指定不同。我们可以将这两个规格结合起来,在特定的里程碑中列出非活动性问题:

image

上表使用 And 扩展方法组合规格。有更多的组合方法可用,如或(…)和和不(…)。

域服务

域服务实现域逻辑,其中:

  • 取决于服务和存储库。
  • 需要处理多个聚合体,因此逻辑无法正确地与任何聚合体配合。

域服务与域对象配合用。他们的方法可以获取和返回实体,值对象,原始类型。等。

但是,它们不会获得/返回 DTO。DTO 是应用层的一部分。

示例:将问题分配给用户

请记住问题实体中如何执行问题分配:

image

在这里,我们将将此逻辑移动到域服务。

首先,更改问题类:

image

  • 删除了分配相关方法。
  • 将分配的UserId属性设置器从私有更改为内部设置,以便从域服务设置它。

下一步是创建名为"问题管理器"的域名服务,该服务已分配给给定用户。

image

问题管理器可以注入任何服务依赖性,并用于查询打开的问题指望用户。

此设计的唯一问题是问题。 分配使用现在开放设置出类。但是,它不公开。

它是内部的,仅在同一组件(问题跟踪.Domain 项目中)内才可能更改此示例解决方案。我们认为这是合理的

  • 域层开发人员已经了解域规则,他们使用问题管理器。
  • 应用程序层开发人员已经强制使用问题管理器,因为它们不直接设置它

虽然两种方法之间存在权衡,但我们更喜欢在业务逻辑需要与外部服务配合时创建域服务。

示例:将问题分配给用户

image

应用服务方法通常有三个步骤, 这些步骤在此处实施:

  • 从数据库获取相关域对象以实现使用案例。
  • 使用域对象(域服务、实体等)执行实际操作。
  • 更新数据库中已更改的实体。

数据传输对象

DTO 是用于在应用层和演示层之间传输状态(数据)的简单对象。

因此,应用程序服务方法获取并返回 DTO。

常见的 DTO 原则和最佳实践

  • DTO 应按其性质进行序列化。因为,大多数时候它是通过网络传输的。因此,它应该有一个无参数(空)构造器。
  • 不应包含任何业务逻辑。
  • 切勿继承或提及实体。

输入 DTO(这些被传递到应用服务方法)的性质与输出 DTO(这些从应用服务方法返回)不同。因此,他们将受到不同的对待。

输入 DTO 最佳实践

不要为输入 DTO 定义未使用的属性

仅定义使用案例所需的属性!否则,客户使用应用服务方法会让人感到困惑。您当然可以定义可选属性,但当客户端提供它们时,它们应该会影响使用案例的工作原理。

这条规则似乎没有必要首先。谁将定义一种方法未使用的参数(输入 DTO 属性)?但它发生,尤其是当你试图重复使用输入D托。

不要重复使用输入 DTO

为每个用例定义专用输入 DTO(应用服务方法)。否则,在某些情况下不使用某些属性,这违反了上述规则:不要使用

定义未使用的属性用于输入 DTO。

有时,对于两个用例重复使用相同的 DTO 类似乎很有吸引力,因为它们几乎相同。即使他们现在一样,他们可能会变得不同的时候,你会来到同样的问题。代码重复比耦合使用案例更适合使用。

重复使用输入 DTO 的另一种方法是 相互继承 DTO。虽然这在一些罕见情况下是有用的,但大多数时候,它带您到同一点。

image

IUserAppService 使用用户 Dto 作为所有方法(使用案例)中的输入 DTO。用户Dto 定义如下:

image

此示例:

  • 由于服务器确定 ID,因此"创建"中不使用 ID。
  • 密码不在"更新"中使用,因为我们有另一种方法。
  • 创建时间永远不会使用,因为我们不能允许客户发送创建时间。它应设置在服务器中。

真正的实现可以是这样的:

image

使用给定的输入 DTO 类:

image

输入 DTO 验证逻辑

  • 仅在 DTO 内部实施正式验证。使用数据注释验证属性或实施 IValidab 对象进行正式验证。
  • 不要执行域验证。例如,不要尝试检查 DTO 中唯一的用户名约束。

示例:使用数据注释属性

image

ABP 框架自动验证输入 DTO,抛出 Abp 验证例外,并在输入无效的情况下将 HTTP 状态 400 返回给客户。

输出 DTO 最佳实践

  • 保持输出 DTO 计数最小。尽可能重复使用(例外:不要将输入 DTO 重用为输出 DTO)。
  • 输出 DTO 可以包含比客户端代码中使用的更多的属性。
  • 从创建和更新方法返回实体 DTO。

示例:从不同方法返回不同的 DTO

image

上面的示例代码返回每个 方法的不同 DTO 类型。正如您所猜到的,查询数据、将实体 映射到 DTO 将有很多代码重复。

以上 IUser 应用服务可简化:

image

单输出 DTO:

image

  • 删除 Getuser 名邮件和 Getroles, 因为 Get 方法已经返回了必要的信息。
  • 获取列表现在返回相同的获取。
  • 创建和更新还返回相同的用户数据。