基于ABP实现DDD--聚合和聚合根实践
在下面的例子中涉及Repository、Issue、Label、User这4个聚合根,接下来以Issue聚合为例进行分析,其中Issue聚合是由Issue[聚合根]、Comment[实体]、IssueLabel[值对象]组成的集合。
1.单个单元原则
简单理解,一个聚合就是由实体和值对象组成的集合,通过聚合根将所有关联对象绑定在一起,一个聚合是一个相对独立的业务单元。聚合和聚合根原则包括:包含业务原则,单个单元原则,事务边界原则,可序列化原则。接下来通过例子重点介绍下什么是单个单元原则,本质上是为了实现业务规则并保持数据的一致性和完整性。比如,要向Issue中添加Comment,操作如下:
- 通过聚合根Issue加载所有的实体Comments[该问题的评论列表]和值对象IssueLabels[该问题的标签集合]等。
- 在Issue类中有个AddComment()方法可以添加一个新的Comment。
- 通过数据库更新操作将Issue聚合,包括实体和值对象等保存到数据库。
添加Comment到Issue如下所示:
public class IssueAppService : ApplicationService, IIssueAppService
{
private readonly IRepository<IssueAppService, Guid> _issueRepository;
public IssueAppService(IRepository<Issue, Guid> issueRepository)
{
_issueRepository = issueRepository;
}
[Authorize]
public async Task CreateCommentAsync(CreateCommentInput input)
{
// 加载Issue对象并包含所有子集合
var issue = await _issueRepository.GetAsync(input.IssueId);
// 哪个用户评论了什么内容
issue.AddComment(CurrentUser.GetId(), input.Text);
// 保存更改到数据库,执行完后自动调用DbContext.SaveChanges()
await _issueRepository.UpdateAsync(issue);
}
}
2.只通过ID引用其它聚合
Repository和Issue的关系是一对多,即一个Repository对应多个Issue:
public class GitRepository : AggregateRoot<Guid>
{
public string name { get; set; }
public int StarCount { get; set; }
public Collection<Issue> Issues { get; set; } //错误实践,不能添加导航属性到其它聚合根
}
public class Issue : AggregateRoot<Guid>
{
public string Text { get; set; }
public GitRepository Repository { get; set; } //错误实践,不能添加导航属性到其它聚合根
public Guid RepositoryId { get; set; } //正确实践
}
3.聚合根要足够小
因为一个聚合将做为一个整体被加载和保存,如果聚合根很大,在读写一个大对象的时候会影响到性能问题。
using Microsoft.VisualBasic;
public class UserRole : ValueObject //值对象
{
public Guid UserId { get; set; }
public Guid RoleId { get; set; }
}
public class Role : AggregateRoot<Guid>
{
public string Name { get; set; }
public Collection<UserRole> Users { get; set; } //错误实践,理由是角色对应的用户是增加的
}
public class User : AggregateRoot<Guid>
{
public string Name { get; set; }
public Collection<UserRole> Roles { get; set; } //正确实践,理由是用户对应的角色总是有限的
}
官方的建议是一个子集合最多不应包含超过100-150条记录,否则建议为实体单独提取为一个新的聚合根。
4.聚合根/实体中的主键
聚合根通常使用Guid作为主键,聚合根中的实体[不是聚合根]可以使用复合主键。
// 聚合根:单个主键
public class Organization
{
public Guid Id { get; set; }
public string Name { get; set; }
// ...
}
// 实体:复合主键[值对象]
public class OrganizationUser
{
public Guid OrganizationId { get; set; } //主键
public Guid UserId { get; set; } //主键
public bool IsOwner { get; set; }
// ...
}
一般聚合根中的实体[不是聚合根]是单个主键的,而值对象基本都是复合主键,比如IssueLabel,通过复合主键关联Issue和Label这2个聚合根。
5.业务逻辑和实体中的异常处理
假定有2个业务原则:第1个是锁定的Issue不能重新打开,第2个是不能锁定一个关闭的Issue:
public class Issue:AggregateRoot<Guid>
{
//...
public bool IsLocked {get;private set;}
public bool IsClosed{get;private set;}
public IssueCloseReason? CloseReason {get;private set;}
public void Close(IssueCloseReason reason)
{
IsClose = true;
CloseReason =reason;
}
public void ReOpen() //重新打开
{
if(IsLocked)
{
throw new IssueStateException("不能打开⼀个锁定的问题!请先解锁!");
}
IsClosed=false;
CloseReason=null;
}
public void Lock() //锁定
{
if(!IsClosed)
{
throw new IssueStateException("不能锁定⼀个关闭的问题!请先打开!");
}
}
public void Unlock() //解锁
{
IsLocked = false;
}
}
这时会遇到2个问题,一个是异常消息本地化,另一个是HTTP状态码。通过ABP的异常处理系统可以解决这些问题,即IssueStateException类继承自BusinessException类[1]。重写ReOpen方法:
public void ReOpen()
{
if (IsLocked)
{
throw new IssueStateException("IssueTracking:CanNotOpenLockedIssue");
}
IsClosed = false;
CloseReason = null;
}
为了实现本地化消息处理,只用在本地化资源中添加"IssueTracking:CanNotOpenLockedIssue":"不能打开⼀个锁定的问题!请先解锁!"即可。HTTP状态码在BusinessException类中已经处理好了,比如403表示请求禁用,500表示服务器内部错误等。
6.实体中业务逻辑需要用到外部服务
假如业务规则是:一个用户不能同时分配超过3个未解决的问题。这时就需要一个服务,根据User的Id获取已经分配的未解决问题的数目。如何在实体类中实现它呢?暂时解决问题的思路是将外部依赖项作为方法的参数:
public class Issue : AggregateRoot<Guid>
{
// ...
public Guid? AssignedUserId { get; private set; } //将实体属性访问器设置私有,这样只能通过方法来访问
// 问题分配方法
// IUserIssueService:用于获取分配给用户的未解决问题的数量
public async Task AssignToAsync(AppUser user, IUserIssueService userIssueService)
{
var openIssueCount = await userIssueService.GetOpenIssueCountAsync(user.Id);
if (openIssueCount >= 3)
{
throw new BusinessException("IssueTracking:CanNotOpenLockedIssue");
}
AssignedUserId = user.Id;
}
// 清空分配方法
public void CleanAssignment()
{
AssignedUserId = null;
}
}
这种实现方式虽然满足了业务实现,但是实体变的复杂且难用,一方面实体类依赖外部服务,另一方面在调用方法AssignToAsync的时候需要注入依赖的外部服务IUserIssueService作为参数。比较优雅的实现此业务逻辑的方式是引入领域服务。
说明:聚合和聚合根最佳实践中的用于EF Core和关系型数据库、聚合根/实体构造函数、实体属性访问器和方法这3个部分就不介绍了,感兴趣参考《基于ABP Framework实现领域驱动设计》[2]。
参考文献:
[1]ABP异常处理:https://docs.abp.io/zh-Hans/abp/latest/Exception-Handling
[2]基于ABP Framework实现领域驱动设计:https://url39.ctfile.com/f/2501739-616007877-f3e258?p=2096 (访问密码: 2096)
基于ABP实现DDD--聚合和聚合根实践的更多相关文章
- 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则
目录 前言 聚合 聚合和聚合根原则 包含业务原则 单个单元原则 事务边界原则 可序列化原则 聚合和聚合根最佳实践 只通过ID引用其他聚合 用于 EF Core 和 关系型数据库 保持聚合根足够小 聚合 ...
- 从壹开始微服务 [ DDD ] 之六 ║聚合 与 聚合根 (下)
前言 哈喽大家周二好,上次咱们说到了实体与值对象的简单知识,相信大家也是稍微有些了解,其实实体咱们平时用的很多了,基本可以和数据库表进行联系,只不过值对象可能不是很熟悉,值对象简单来说就是在DDD领域 ...
- DDD之4聚合和聚合根
聚合就是归类的意思,把同类事物统一处理: 聚合根也就是最抽象,最普遍的特性: 背景 领域建模的过程回顾: 那么问题来了? 为什么要在限界上下文和实体之间增加聚合和聚合根的概念,即作用是什么? 如何设计 ...
- DDD中聚合、聚合根的含义以及作用
聚合与聚合根的含义 聚合: 聚合往往是一些实体为了某项业务而聚类在一起形成的集合 , 举个例子, 社会是由一个个的个体组成的,象征着我们每一个人.随着社会的发展,慢慢出现了社团.机构.部门等组织,我们 ...
- 关于领域驱动设计(DDD)中聚合设计的一些思考
关于DDD的理论知识总结,可参考这篇文章. DDD社区官网上一篇关于聚合设计的几个原则的简单讨论: 文章地址:http://dddcommunity.org/library/vernon_2011/, ...
- ASP.NET Core Web API下事件驱动型架构的实现(四):CQRS架构中聚合与聚合根的实现
在前面两篇文章中,我详细介绍了基本事件系统的实现,包括事件派发和订阅.通过事件处理器执行上下文来解决对象生命周期问题,以及一个基于RabbitMQ的事件总线的实现.接下来对于事件驱动型架构的讨论,就需 ...
- NET Core Web API下事件驱动型架构CQRS架构中聚合与聚合根的实现
NET Core Web API下事件驱动型架构在前面两篇文章中,我详细介绍了基本事件系统的实现,包括事件派发和订阅.通过事件处理器执行上下文来解决对象生命周期问题,以及一个基于RabbitMQ的事件 ...
- [2018-12-07]用ABP入门DDD
前言 ABP框架一直以来都是用DDD(领域驱动设计)作为宣传点之一.但是用过ABP的人都知道,ABP并不是一个严格遵循DDD的开发框架,又或者说,它并没有完整实现DDD的所有概念. 但是反过来说,认真 ...
- 基于ABP落地领域驱动设计-01.全景图
什么是领域驱动设计? 领域驱动设计(简称:DDD)是一种针对复杂需求的软件开发方法.将软件实现与不断发展的模型联系起来,专注于核心领域逻辑,而不是基础设施细节.DDD适用于复杂领域和大规模应用,而不是 ...
随机推荐
- 干货长文:Linux 文件系统与持久性内存介绍
关注「开源Linux」,选择"设为星标" 回复「学习」,有我为您特别筛选的学习资料~ 1.Linux 虚拟文件系统介绍 在 Linux 系统中一切皆文件,除了通常所说的狭义的文件以 ...
- 电脑UEFI启动是什么?
UEFI 当EFI发展到1.1的时候,英特尔决定把EFI公之于众,于是后续的2.0吸引了众多公司加入,EFI也不再属于英特尔,而是属于了Unified EFI Form的国际组织,EFI在2.0后也遂 ...
- Linux screen命令详解
开源Linux 长按二维码加关注~ 上一篇:局域网IP冲突罪魁祸首是谁? 很多时候,我们都需要执行一些需要很长时间的任务.如果这时候,你的网络连接突然断开了,那么你之前所做的所有工作可能都会丢失,所做 ...
- 2021 CSP-J复赛 我的备战与游记
目录 备战 2021.10.18 2021.10.19 2021.10.20 2021.10.21 2021.10.22 比赛当日 早上 线下见面 正文 比赛后 赛后总结与讲解 简单总结 Candy ...
- p2p-tunnel 打洞内网穿透系列(一)客户端配置及打洞
系列文章 p2p-tunnel 打洞内网穿透系列(一)客户端配置及打洞 p2p-tunnel 打洞内网穿透系列(二)TCP转发访问远程共享文件夹 p2p-tunnel 打洞内网穿透系列(三)TCP转发 ...
- 2. 假设当前文件夹中data.csv文件中存放了2020年某饭店营业额,第一列为日期(如2020-02-03),第二列为每天交易额(如3560),文件中第一行为表头,其余行为实 际数据。
假设当前文件夹中data.csv文件中存放了2020年某饭店营业额,第一列为日期(如2020-02-03),第二列为每天交易额(如3560),文件中第一行为表头,其余行为实 际数据.编写程序,完成下 ...
- 手把手教你 bash中给变量赋值时 ' 和 " 和 ` 和 $() 的使用
1.赋值指令 var='变量内容' var="变量内容" var=`command` var=$(command) var=变量内容 2.格式要求 =两边不能有空白字符 错误示例 ...
- 385. Mini Parser - LeetCode
Question 385. Mini Parser Solution 分析:用NI(count,list)来表示NestedInteger,则解析字符串[123,[456,[789]]]过程如下: # ...
- 11┃音视频直播系统之 WebRTC 进行文本聊天并实时传输文件
一.RTCDataChannel WebRTC 不但可以让你进行音视频通话,而且还可以用它传输普通的二进制数据,比如说可以利用它实现文本聊天.文件的传输等 WebRTC 的数据通道(RTCDataCh ...
- 使用 gitbook 制作自己的 html 文档
使用 gitbook 制作自己的 html 文档 步骤如下 npm install gitbook-cli -g // 全局安装 gitbook-cli <span style="te ...