DDD013-实现领域驱动设计:第二部分
实现:构建基块
这是本系列的重要组成部分。我们将用实例介绍和解释一些明确的规则。您可以遵循这些规则,并在实施域驱动设计时应用到您的解决方案中。
示例领域
示例将使用 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 是必要的。我们 将其保密,以防止意外地使用它在我们自己的 代码。
实体属性访问器和方法
上面的例子可能看起来很奇怪!例如,我们强制在构造器中传递非空标题。
但是,开发商可以在没有任何控制的情况下将标题属性设置为无效。这是因为上面的示例代码只关注构造器。
如果我们向公共设置者申报所有属性(如上文的示例问题类),则不能强制实体在其生命周期中的有效性和完整性。
所以:
- 当您在设置该属性时需要执行任何逻辑时,请使用私人设置器进行属性设置。
- 定义操作此类属性的公共方法。
示例:以受控 方式更改属性的方法
- 存储库设置器是私密的,在创建问题后无法更改它,因为这是我们在这个领域想要的:问题不能移动到另一个存储库。
- 文本和分配使用者具有公共设置器,因为对它们没有限制。它们可以是空的或任何其他值。我们认为没有必要定义单独的方法来设置它们。如果以后需要,我们可以添加方法,使设置器保密。域层的中断更改不是问题,因为域层是一个内部项目,它不会暴露给客户端。
- 关闭和发行关闭是对属性。定义关闭和重新开机的方法,以一起更改它们。这样,我们就无法无缘无故地解决问题。
业务逻辑与实体中的例外情况
当您在实体中实施验证和业务逻辑时,您经常需要管理特殊案例。
- 创建域特定例外。
- 必要时将这些例外情况放入实体方法中。
这里有两个业务规则:
- 锁定的问题无法重新打开。
- 您无法锁定未结问题。
在这些情况下,问题类会抛出问题状态例外,以 强制执行业务规则:
抛出这种例外有两个潜在的问题:
- 如果出现此类异常,最终用户应看到异常(错误)消息吗?如果是,您如何定位异常消息?您不能使用本地化系统,因为不能在实体中注入和使用 IString 本地化器。
- 对于 Web 应用程序或 HTTP API,HTTP 状态代码应返回给客户端?
ABP 的例外处理系统解决了这些和类似的问题。
示例:抛出代码的业务例外
- 问题状态例外类继承了业务例外类。ABP 默认返回 403(禁止)HTTP 状态代码(而不是 500 - 内部服务器错误),但从业务例外中得出的例外情况。
- 该代码用作本地化资源文件 中的密钥,用于查找本地化消息。
现在,我们可以更改以下重新开机方法:
并在本地化资源中添加一个条目,如下所示:
- 抛出异常时,ABP 会自动使用此本地化消息(基于当前语言)显示给最终用户。
- 例外代码(问题跟踪:此处无法启动锁定问题)也发送给客户端,因此它可以按程序处理错误案例。
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/geek/post/DDD/DDD013-%E5%AE%9E%E7%8E%B0%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E7%AC%AC%E4%BA%8C%E9%83%A8%E5%88%86/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com