基于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适用于复杂领域和大规模应用,而不是 ...
随机推荐
- springboot简单发送邮件介绍
1.新建一个springboot项目 2.所需要的的jar包: <dependency> <groupId>org.springframework.boot</group ...
- python 动态规划(背包问题和最长公共子串)
背包问题 现在要往一个可以装4个单位重量的背包里怎么装价值最高:A重量1个单位,价值15:B重量3个单位,价值20:C重量4个重量,价值30 使用动态规划填充空格 class SolutionBag: ...
- Spring事务源码解读
一.Spring事务使用 1.通过maven方式引入jar包 <dependency> <groupId>com.alibaba</groupId> <art ...
- Spring Boot整合模板引擎jsp
jsp也算是一种模板引擎吧.整合jsp前,先说一下运行SpringBoot项目的几种方式 1. 运行SpringBoot项目的几种方式 1.1 使用内嵌Tomcat运行项目 在IDE中右键运行启动类, ...
- Android.mk编译App源码
在Andriod源码环境编译APP主要考虑如何引入第三方jar包和arr包的问题,初次尝试,步步是坑,这里给出一个模板: LOCAL_PATH := $(call my-dir) include $( ...
- forms组件补充与ModelForm简单使用与cookie与session
目录 forms组件钩子函数 forms组件字段参数 字段参数 validators详解 choices详解 widget详解 forms组件字段类型 ModelForm简单使用 cookie与ses ...
- form表单与css选择器
目录 form表单 action属性 input标签 lable标签 select标签 textarea标签 补充 网络请求方式 CSS简介 CSS基本选择器 组合选择器 属性选择器 分组与嵌套 伪类 ...
- 论文阅读 Predicting Dynamic Embedding Trajectory in Temporal Interaction Networks
6 Predicting Dynamic Embedding Trajectory in Temporal Interaction Networks link:https://arxiv.org/ab ...
- Tmux终端复用神器使用心得
tmux在连接远程服务器做实验时很有帮助,可以为每一个项目设置一个Session,在Session中设置不同的Window同时跑多个实验,Window本身还可以分割为多个Pane,在一个视野内利用多个 ...
- 两个月吃透阿里P9推荐260页SpringBoot2企业应用实战pdf入职定P6+
前言 Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置 ...