实现DDD领域驱动设计: Part 2

原文链接: https://dev.to/salah856/implementing-domain-driven-design-part-ii-2i36

实现:构建块

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

示例

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

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

b2d8558aecbc38fcfb831b7fbfa9faef.png

问题聚合由包含评论和问题标签集合的问题聚合根组成。

其他聚合显示很简单,因此我们将关注问题聚合:

8d70652948f6122faa157dcb448a4a8c.png

聚合

如前所述,聚合是由聚合根对象绑定在一起的一组对象(实体和值对象)。

聚合/聚合根原则

业务规则

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

聚合应通过实现领域规则和约束来保持其自身的完整性和有效性。

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

单个单元

检索聚合并保存为单个单元,其中包含所有子集合和属性。例如,如果你想为问题添加评论,你需要这样做。

  • 从包含所有子集合(评论和问题标签)的数据库中获取问题。

  • 使用Issue类上的方法添加新评论,例如Issue.AddComment(...)。

  • 将问题(包含所有子集合)作为单个数据库操作(更新)保存到数据库中。

对于以前使用EF Core和关系数据库的开发人员来说,这可能看起来很奇怪。

获取所有细节的问题似乎没有必要且效率低下。为什么我们不直接对数据库执行SQL插入命令而不查询任何数据呢?

答案是我们应该实现业务规则并保持代码中的数据一致性和完整性。

如果我们有一个业务规则,比如“用户不能评论锁定问题”,我们如何在不从数据库中检索问题的情况下检查问题的锁定状态?

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

示例:向问题添加评论

9cfb20b776c73b4b1fd8867911133c7b.png

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

虽然这适用于MongoDB,但你需要为EF Core配置聚合详细信息。但是,一旦你进行了配置,存储库就会自动处理它。

_issueRepository.GetAsync方法提供一个可选参数includeDetails,你可以在需要时传递false以禁用此行为。

Issue.AddComment获取userId和评论文本,实现必要的业务规则并将评论添加到问题的Comments集合中。

最后,我们使用_issueRepository.UpdateAsync来保存更改到数据库。

事务边界

聚合通常被视为事务边界。

如果用例使用单个聚合,将其作为单个单元读取和保存,则对聚合对象所做的所有更改都将作为原子操作一起保存,你不需要显式的数据库事务。

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

可序列化

聚合(具有根实体和子集合)应该是可序列化的,并且可以作为单个单元在线传输。

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

以下规则可以保证可序列化性。

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

以下规则确保实施上述原则。

仅按ID引用其他聚合

第一条规则说Aggregate只能通过其Id引用其他聚合。这意味着你不能将导航属性添加到其他聚合。

  • 该规则使得实现可序列化原则成为可能。

  • 它还可以防止不同的聚合相互操纵以及将聚合的业务逻辑泄露给彼此。

在下面的示例中,你会看到两个聚合根GitRepository和Issue:

3c52947aeadd7a9bf248194bb5f21323.png

  • GitRepository不应该有问题的集合,因为它们是不同的聚合。

  • 问题不应具有相关GitRepository的导航属性,因为它是不同的聚合。

  • 问题可以有RepositoryId(作为 Guid)。

因此,当你遇到问题并需要与此问题相关的GitRepository时,你需要通过RepositoryId从数据库中显式查询它。

保持较小的聚合

一种好的做法是使聚合保持简单和小。

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

c9c00c3f6e83f585dff16ff8f990f6c5.png

角色聚合具有一组UserRole值对象,用于跟踪分配给该角色的用户。

请注意,UserRole不是另一个聚合,对于“仅按 Id 引用其他聚合”规则来说这不是问题。

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

聚合根/实体上的主键

  • 聚合根通常有一个Id属性作为它的标识符(Primark Key:PK)。我们更喜欢Guid作为聚合根实体的PK。

  • 聚合中的实体(不是聚合根)可以使用复合主键。

    5d375889cec245c465c9c37cb85c9c9e.png

  • Organization有一个Guid标识符(Id)。

  • OrganizationUser是Organization的子集合,具有由OrganizationId和UserId组成的复合主键。

聚合根/实体的构造函数

构造函数位于实体生命周期开始的位置。精心设计的构造函数有一些职责:

  • 获取所需的实体属性作为参数以创建有效实体。应该强制只传递必需的参数,并且可能将非必需的属性作为可选参数。

  • 检查参数的有效性。

  • 初始化子集合。

    4512b487ad4a6dd51f4b7e1c8b75d525.png

  • 问题类通过在其构造函数中获取最小必需属性作为参数来正确强制创建有效实体。

  • 构造函数验证输入(如果给定值为空,Check.NotNullOrWhiteSpace(...) 将抛出 ArgumentException)。

  • 它初始化子集合,因此在创建问题后尝试使用标签集合时不会出现空引用异常。

  • 构造函数还获取id并传递给基类。我们不会在构造函数中生成Guid,以便能够将此责任委托给另一个服务。

  • ORM需要私有的空构造函数。我们将其设为私有以防止在我们自己的代码中意外使用它。

实体属性访问器和方法

上面的例子对你来说可能很奇怪!例如,我们强制在构造函数中传递一个非空的Title。

但是,开发人员可以在没有任何控制的情况下将Title属性设置为null。这是因为上面的示例代码只关注构造函数。

如果我们用公共设置器声明所有属性(如上面的示例问题类),我们不能强制实体在其生命周期中的有效性和完整性。

所以:

当你需要在设置该属性时执行任何逻辑时,请为该属性使用私有setter。

定义公共方法来操作这些属性。

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

afd33e900338ea05a6d6f056c14df867.png

  • RepositoryId setter 设为私有,在创建问题后无法更改它,因为这是我们在此领域中想要的:无法将问题移动到另一个存储库。

  • Text和AssignedUserId具有公共设置器,因为它们没有限制。它们可以是null或任何其他值。我们认为没有必要定义单独的方法来设置它们。如果我们以后需要,我们可以添加方法并使设置器私有。由于领域层是一个内部项目,它不会暴露给客户,因此在领域层中进行重大更改不是问题。

  • IsClosed和IssueCloseReason是成对属性。定义了Close和ReOpen方法来一起改变它们。通过这种方式,我们可以防止无故关闭问题。

实体中的业务逻辑和异常

在实体中实现验证和业务逻辑时,你经常需要管理异常情况。

  • 创建特定领域的例外。

  • 必要时在实体方法中抛出这些异常。

00726162ce9e1d6c47755888054b64ce.png

这里有两个业务规则:

  • 已锁定的问题无法重新打开。

  • 你不能锁定未解决的问题。

在这些情况下,问题类会抛出一个IssueStateException强制业务规则:

d9b019988f3c57726fe338d9ede03489.png

抛出此类异常有两个潜在问题;

  1. 如果出现此类异常,最终用户是否应该看到异常(错误)消息?如果是这样,你如何本地化异常消息?你不能使用本地化系统,因为你不能在实体中注入和使用IStringLocalizer。

  2. 对于Web应用程序或HTTP API,应该向客户端返回什么HTTP状态代码?

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

示例:使用代码引发业务异常

eb4355fc9c4fbd1293913970aca52eca.png

  • IssueStateException类继承了BusinessException类。对于从BusinessException派生的异常,ABP默认返回403(禁止)HTTP 状态代码(而不是500 - 内部服务器错误)。

  • 该代码用作本地化资源文件中的键以查找本地化消息。

现在,我们可以更改ReOpen方法,如下所示:

7e27161c7277d8739899c19c19c75d0c.png

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

bb133fc84d29328eb4485fde0007f5f5.png

  • 当你抛出异常时,ABP会自动使用此本地化消息(基于当前语言)向最终用户显示。

  • 异常代码(此处为IssueTracking:CanNotOpenLockedIssue)也被发送到客户端,因此它可以以编程方式处理错误情况。

如果你觉得这篇文章对你有所启发,请关注我的个人公众号”My IO“

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/291019.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

王彪20162321 2016-2017-2 《程序设计与数据结构》第5周学习总结

王彪 2016-2017-2 《程序设计与数据结构》第5周学习总结 教材学习内容总结 1.关键概念 1.面向对象程序设计的核心是类的定义,它代表了状态和行为的对象。2.变量的作用域依赖于变量声明的位置,作用域决定在哪里可以使用变量。3.对象应该是封装的&#xff…

ubuntukylin14安装ns-allinone-2.35教程(虚拟机ubuntu同理)

准备材料: 1.ubuntukylin14,百度进官网自行下载; 2.ns-allinone-2.35.tar.gz,百度进官网自行下载; 3.虚拟机:vmwareworkstation(可选). 4.不推荐windows环境下的cygwinns2&#xff0…

C语言之理解(*(void (*)())0)()和signal函数

1、需要理解的表达式 计算机启动的时候,硬件将要调用首地址为0位置的子例程,表达式如下 (*(void (*)())0)(); 2、解释 我们知道void (*f)() f是指向返回值为void类型的函数 因此,(void (*)())表示指向返回值为void类型的函数的指针的类型强制转换符 我们调用函数指针的时…

c语言指针索引数组,C语言数组指针表示法

指针在处理数组时很有用,我们可以用指针指向已有的数组,也可以从堆上分配内存然后把这块内存当做一个数组使用。数组表示法和指针表示法在某种意义上可以互换。不过,它们并不完全相同,后面的“数组和指针的差别”中会详细说明。单…

关于我自己的三个层次

有时候会意识到自己不够聪明,然后在把自己同那些大家相比时会感到自己的渺小,比如说博弈论专家纳什在20世界50年代就弄清楚的博弈论我现在学起来还是感觉很晦涩难懂,这就是自己的天分确实不是很好的直接证明。 天分好不好不是决定性因素&…

C# 使用AggregateException 信息

为了得到所有失败任务的异常信息,可以将 Task.WhenAll 返回的结果写到一个Task 变量中。这个任务会一直等到所有任务都结束。否则,仍然可能错过抛出的异常。上一小节中,catch 语句只检索到第一个任务的异常。不过,现在可以访问外部…

简单动态规划问题分析

例题: 1022: 菜鸟和大牛(csuoj) 像这一类问题,首先不管是属于什么类型的,如果是按照题目的思路一步步走下来,然后运行,最后肯定是要超时的,究其原因,它的时间复杂度很不合…

Android之内置和外置sdcard路径显示并且写入数据

1、效果图片 2、部分代码 package com.example.sdcardcheck;import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Array; import java.lang.…

数据挖掘——数据仓库

虽然存在数据仓库并不是数据挖掘的先决条件,但实际上,若能访问数据仓库,数据挖掘的任务就会变得容易的多。 数据仓库的主要目标是增加决策过程的“情报”和此过程的相关人员的知识。数据仓库对不同的人来说有不同的意义。 数据仓库是一个集成…

c2000 汇编语言指令,C2000系CMD文件的配置理解

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。您需要 登录 才可以下载或查看,没有帐号?注册x推荐CMD的专业名称叫链接器配置文件,是存放链接器的配置信息的,我们简称为命令文件&#…

OxyPlot 导出图片及 WPF 元素导出为图片的方法

OxyPlot 导出图片及 WPF 元素导出为图片的方法目录OxyPlot 导出图片及 WPF 元素导出为图片的方法一、OxyPlot 自带导出方法二、导出 WPF 界面元素的方法三、通过附加属性来使用独立观察员 2022 年 2 月 26 日最近有个需求,就是将 OxyPlot 图形导出图片。经过尝试&am…

java实现各种算法

package sortAlgorithm;import java.io.File;import java.io.IOException;import java.sql.Time;import java.util.Random;* author sky* 该类给出各种排序算法public class sort{private static Integer[] elem(int n){int Nn;Random randomnew Random();Integer elem[]new In…

BDB (Berkeley DB)简要数据库(转载)

使用最近DBD。然后搜了下相关资料,首先公布的是一门科学: 转会http://www.javaeye.com/topic/202990 DB综述DB最初开发的目的是以新的HASH訪问算法来取代旧的hsearch函数和大量的dbm实现(如AT&T的dbm,Berkeley的ndbm。GNU项目…

C语言之rand()和srand()函数

1、rand()、srand()函数介绍 srand 初始化随机种子,rand 产生随机数 定义函数:int rand(void) 函数功能:产生随机数 函数说明:rand的内部实现是用线性同余法做的,不是真的随机数,因为其周期特别长,一定的范围里可看成是随机的。rand()会返回一随机数值,范围在0至RAND_…

delphi中利用Indy的TIdFtp控件实现FTP协议

2019独角兽企业重金招聘Python工程师标准>>> delphi中利用Indy的TIdFtp控件实现FTP协议版权声明:本文为博主原创文章,未经博主允许不得转载。现在很多应用都需要上传与下载大型文件,通过HTTP方式上传大文件有一定的局限性。幸好FT…

一篇文章了解Liquid模版引擎

背景平常比如开发个代码生成器一般是定一个模板,然后里面很多变量,根据数据生成文件。这时候模版引擎就派上了用场。下面我们来介绍下Liquid。什么是Liquid 呢,就像java、c#等编程语言一样,Liquid也是一种独立的语言,没…

C++之map插入数据相同的key不能覆盖value解决办法

1、问题 C里面,如果map里面插入之前的<key, value>,如果key在map里面有的话&#xff0c;不会覆盖之前的value,一般先判断之前有没有数据&#xff0c;有的话先删除&#xff0c;然后再去添加。 2、代码实现 3、运行结果

【BZOJ】【4145】【AMPPZ2014】The Prices

状压DP/01背包 Orz Gromah 容易发现m的范围很小……只有16&#xff0c;那么就可以状压&#xff0c;用一个二进制数来表示买了的物品的集合。 一种简单直接的想法是&#xff1a;令$f[i][j]$表示前$i$个商店买了状态集合为$j$的商品的最小代价&#xff0c;那么我们转移的时候就需…

Java中的String,StringBuilder,StringBuffer三者的区别

最近在学习Java的时候&#xff0c;遇到了这样一个问题&#xff0c;就是String,StringBuilder以及StringBuffer这三个类之间有什么区别呢&#xff0c;自己从网上搜索了一些资料&#xff0c;有所了解了之后在这里整理一下&#xff0c;便于大家观看&#xff0c;也便于加深自己学习…

WPF 实现人脸检测

WPF开发者QQ群此群已满340500857 &#xff0c;请加新群458041663由于微信群人数太多入群请添加小编微信号yanjinhuawechat 或 W_Feng_aiQ 邀请入群需备注WPF开发者 PS&#xff1a;有更好的方式欢迎推荐。接着上一篇利用已经训练好的数据文件,检测人脸 地址如下&#xff1a;http…