实现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…

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

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

C# 使用AggregateException 信息

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

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.…

数据挖掘——数据仓库

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

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

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

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

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

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;那么我们转移的时候就需…

WPF 实现人脸检测

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

C++之函数的默认值参数说明

1、思考 今天看到C代码的时候&#xff0c;发现文件里面的函数定义和实现都有3个参数&#xff0c;特码调用的时候只有2个参数了&#xff0c;日了狗&#xff0c;java里面好像没有这种方式&#xff0c;后来才发现是默认参数 2、代码实现 3、展示结果 4、总结 注意默认参数需要写…

插头DP

AC HDU1693 不能再简单了的插头DP 1 #include <cstdio>2 #include <fstream>3 #include <iostream>4 5 #include <cstdlib>6 #include <cstring>7 #include <algorithm>8 #include <cmath>9 10 #include <queue>11 #include…

自定义控件详解(四):Paint 画笔路径效果

Paint 画笔 &#xff0c;即用来绘制图形的"笔" 前面我们知道了Paint的一些基本用法&#xff1a; paint.setAntiAlias(true);//抗锯齿功能 paint.setColor(Color.RED); //设置画笔颜色 paint.setStyle(Style.FILL);//设置填充样式 paint.setStrokeWidth(10);//设…

2021 .NET Conf China 主题分享之-轻松玩转.NET大规模版本升级

去年.NET Conf China 技术大会上&#xff0c;我给大家分享了主题《轻松玩转.NET大规模版本升级》&#xff0c;今天把具体分享的内容整理成一篇博客&#xff0c;供大家研究参考学习。一、先说一下技术挑战和业务背景我们公司&#xff1a;特来电新能源股份有限公司&#xff1a;中…

ASP.NET Core基于滑动窗口算法实现限流控制

前言在实际项目中&#xff0c;为了保障服务器的稳定运行&#xff0c;需要对接口的可访问频次进行限流控制&#xff0c;避免因客户端频繁请求导致服务器压力过大。而AspNetCoreRateLimit[1]是目前ASP.NET Core下最常用的限流解决方案。查看它的实现代码&#xff0c;我发现它使用…

linux操作系统cp命令

转载于:https://www.cnblogs.com/skl374199080/p/3863918.html

sql必读的九本书

2019独角兽企业重金招聘Python工程师标准>>> 原文地址 直接上书(书籍以后会陆续加上去)书籍下载地址 《MySQL必知必会》《SQL学习指南&#xff08;第2版 修订版&#xff09;》《MySQL技术内幕——InnoDB存储引擎》《Redis设计与实现》《ZooKeeper&#xff1a;分布式…

C语言之加入头文件<stdbool.h>可以使用true和false

1、头文件<stdbool.h>介绍 &#xff08;1&#xff09;使用了<stdbool.h>后&#xff0c;可使用true和false来表示真假。 &#xff08;2&#xff09;在循环语句中进行变量声明是C99中才有的&#xff0c;因此编译时显式指明 gcc -stdc99 prime.c 2、最简单的例子 3、…

Nginx负载均衡+转发策略

负载均衡负载均衡(详解)https://cloud.tencent.com/developer/article/1526664--示例1upstream www_server_pool { server 10.0.0.5; server 10.0.0.6&#xff1a;80 weight1 max_fails1 fails_timeout10s; server 10.0.0.7&#xff1a;80 weight1 max_fails2 fails_timeo…

教育行业的互联网焦虑症

2019独角兽企业重金招聘Python工程师标准>>> 文/阑夕 2007年&#xff0c;前新东方名师刘一男在新东方在线&#xff08;网校&#xff09;上的全年课程收入是三千元&#xff0c;四年之后的2011年&#xff0c;这个数字飙升到了四十万&#xff0c;已经和刘一男当年实体…