基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则
围绕DDD和ABP Framework两个核心技术,后面还会陆续发布核心构件实现、综合案例实现系列文章,敬请关注!
ABP Framework 研习社(QQ群:726299208)
ABP Framework 学习及实施DDD经验分享;示例源码、电子书共享,欢迎加入!
系列文章
仓储
仓储(接口)是一组集合的接口,被领域层和应用层用来访问数据持久化系统(数据库),以读写业务对象,业务对象通常是聚合。
仓储的通用原则
- 在领域层中定义仓储接口,在基础层中实现仓储接口(比如:
EntityFrameworkCore项目或MongoDB项目) - 仓储不包含业务逻辑,专注数据处理。
- 仓储接口应该保持 数据提供程序/ORM 独立性。举个例子,仓储接口定义的方法不能返回
DbSet对象,因为该对象由 EF Core 提供,如果使用MongoDB数据库则无法实现该接口。 - 为聚合根创建对应仓储,而不是所有实体。因为子集合实体(聚合)应该通过聚合根访问。
仓储中不包含领域逻辑
虽然这个规则一开始看起来很好理解,但在实际开发过程中,很容易在不经意间将业务逻辑放到仓储中。
示例:从仓储中获取 inactive 状态的 Issue
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
namespace IssueTracking.Issues
{
public interface IIssueRepository:IRepository<Issue,Guid>
{
Task<List<Issue>> GetInActiveIssuesAsync();
}
}
IIssueRepository 继承 IRepository<Issue,Guid> 接口,添加了 GetInActiveIssuesAsync() 方法。与之对应的聚合根类型是 Issue 类:
public class Issue:AggregateRoot<Guid>,IHasCreationTime
{
public bool IsClosed{get;private set;}
public Guid? AssignedUserId{get;private set;}
public DateTime CreationTime{get;private set;}
public DateTime? LastCommentTime{get;private set;}
}
规则要求我们:仓储不应该知道业务规则,那么问题来了:什么是 inactive Issue(未激活的问题)?这是业务规则。
为了更好地理解,我们继续看看接口方法的实现:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using IssueTracking.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
namespace IssumeTracking.Issues
{
public class EfCoreIssueRepository:
EfCoreRepository<IssueTrackingDbContext,Issue,Guid>,
IIssueRepository
{
public EfCoreIssueRepository(
IDbContextProvider<IssueTrackingDbContext> dbContextProvider
):base(dbContextProvider)
{}
public async Task<List<Issue>> GetInActiveIssueAsynce()
{
var daysAgo30=DateTime.Now.Subtract(TimeSpan.FromDays(30));
var dbSet =await GetDbSetAsync();
return await dbSet.Where(i=>
//打开状态
!i.IsClosed &&
//无分配人
i.AssingedUserId ==null &&
//创建时间在30天前
i.CreationTime < daysAgo30 &&
//没有评论或最后一次评论在30天前
(i.LastCommentTime == null || i.LastCommentTime < daysAgo30)
).ToListAsync();
}
}
}
在 GetInActiveIssueAsynce 实现方法中,对于未激活的Issue 这条业务规则,需要满足条件:打开状态、未分配给任何人、创建超过30天、最近30天没有评论。
如果我们将业务规则隐含在仓储中,当我们需要重复使用这个业务逻辑时,问题就出现了。
举个例子,在 Issue 实体中希望添加一个方法 bool IsInActive(),用于检测 Issue 是否未激活状态。
看看如何实现:
public class Issue:AggregateRoot<Guid>,IHasCreationTime
{
public bool IsClosed {get;private set;}
public Guid? AssignedUserId{get;private set;}
public DateTime CreationTiem{get;private set;}
public DateTime? LastCommentTime{get;private set;}
//...
public bool IsInActive(){
var daysAgo30=DateTime.Now.Subtract(TimeSpan.FromDays(30));
return
//打开状态
!IsClosed &&
//无分配人
AssignedUserId ==null &&
//创建时间在30天前
CreationTime < daysAgo30 &&
//无评论或最后一次评论在30天前
(LastCommentTime == null || LastCommentTime < daysAgo30 );
}
}
我们不得不复制、粘贴、修改代码。如果对未激活的Issue 规则改变了怎么办?我们应该记得同时更新这两个地方。这是业务逻辑重复,代码的坏味道,是相当危险的。
这个问题的一个很好的解决方案就是规约。
规约
规约是一个命名的、可重用的、可组合的和可测试的类,用于根据业务规则过滤领域对象。
ABP框架提供了必要的基础设施,以轻松创建规约并在你的应用程序代码中使用。让我们把 inactive Issue 非活动问题业务规则实现为一个规约类。
using System;
using System.Linq.Expressions;
using Volo.Abp.Specifications;
namespace IssueTracking.Issues
{
public class InActiveIssueSpecification:Specification<Issue>
{
public override Expression<Func<Issue,bool>> ToExpression()
{
var daysAgo30=DateTime.Now.Subtract(TimeSpan.FromDays(30));
return i =>
//打开状态
!i.IsClosed &&
//无分配人
i.AssingedUserId ==null &&
//创建时间超过30天
i.CreationTime < daysAgo30 &&
//没有评论或最后评论超过30天
(i.LastCommentTime == null || i.LastCommentTime < daysAgo30)
}
}
}
Specification<T> 基类可以帮助我们简单地创建规约类,我们可以将仓储中的表达式移到规约中。
现在,可以在 Issue 实体和 EfCoreIssueRepository 类中使用 InActiveIssueSpecification 规约。
在实体中使用规约
Specification类提供了一个IsSatisfiedBy方法,如果给定的对象(实体)满足该规范,则返回true。我们可以重新编写Issue.IsInActive方法,如下所示:
public class Issue:AggregateRoot<Guid>,IHasCreationTime
{
public bool IsClosed{get;private set;}
public Guid? AssignedUserId{get;private set;}
public DateTime CreationTiem{get;private set;}
public DateTime? LastCommentTime{get;private set;}
//...
public bool IsInActive()
{
return new InActiveIssueSpecification().IsSatisfiedBy(this);
}
}
创建一个 InActiveIssueSpecification 新实例,使用其 IsSatisfiedBy 方法,进行规约验证。
在仓储中使用规约
首先,修改仓储接口:
public interface IIssueRepository:IRepository<Issue,Guid>
{
Task<List<Issue>> GetIssuesAsync(ISpecification<Issue> spec);
}
将方法名 GetInActiveIssuesAsync 改为 GetIssuesAsync (命名更加简洁),接收一个规约对象参数。将规约判断的代码逻辑从仓储中移出之后,我们不再需要定义不同的方法来获取不同条件下的Issue,比如:GetAssignedIssues(...) 获取已有分配人的问题列表,GetLockedIssues(...) 获取已锁定问题列表 等。
修改仓储的实现:
public class EfCoreIssueRepository:
EfCoreRepository<IssueTrackingDbContext,Issue,Guid>,
IIssueRepository
{
public EfCoreIssueRepository(
IDbContextProvider<IssueTrackingDbContext> dbContextProvider
):base(dbContextProvider)
{}
public async Task<List<Issue>> GetIssuesAsync(ISpecification<Issue> spec)
{
var dbSet = await GetDbSetAsync();
return await dbSet
.Where(spec.ToExpresion())
.ToListAsync();
}
}
ToExpression()方法返回一个表达式,可以直接作为 Where 方法的参数传递,实现实体过滤。
最后,我们将规约实例,传递给 GetIssuesAsync 方法:
public class IssueAppServie : ApplciationService,IIssueAppService
{
private readonly IIssueRepository _issueRepository;
public IssueAppService (IIssueRepository issueRepository)
{
_issueRepository = issueRepository;
}
public async Task DoItAsync()
{
var issues = await _issueRepository.GetIssuesAsync(
new InActiveIssueSpecification();
);
}
}
默认仓储
实际上,你不需要创建自定义仓储就能使用规约。标准的IRepository 接口已经扩展 IQueryable 接口,所以你可以直接使用标准的LINQ扩展方法。(非常帅气!!!)
public class IssueAppServie : ApplciationService,IIssueAppService
{
private readonly IRepository<Issue,Guid> _issueRepository;
public IssueAppService (IRepository<Issue,Guid> issueRepository)
{
_issueRepository = issueRepository;
}
public async Task DoItAsync()
{
var queryable = await _issueRepository.GetQueryableAsync();
var issues = AsyncExecuter.ToListAsync(
queryable.Where(new InActiveIssueSpecification())
);
}
}
AsyncExecuter是ABP框架提供的一个工具类,用于使用异步LINQ扩展方法(比如这里的ToListAsync),而不依赖于EF Core NuGet 包。
组合规约
规范的一个强大的地方是它们是可以组合使用的。假设我们有另一个规约,当问题 Issue 处于指定里程碑中时返回true。
public class MilestoneSpecification : Specification<Issue>
{
public Guid MilestoneId{get;}
public MilestoneSpecification (Guid milestoneId)
{
MilestoneId = milestoneId;
}
public override Expression<Func<Issue,bool>> ToExpression()
{
return i => i.MilestoneId == MilestoneId;
}
}
我们新定义了一个新的参数化规约,和前面定义 InActiveIssueSpecification 不同。那么如何组合两个规约,获取指定里程碑中未激活的 Issue(问题)呢?
public class IssueAppServie : ApplciationService,IIssueAppService
{
private readonly IRepository<Issue,Guid> _issueRepository;
public IssueAppService (IRepository<Issue,Guid> issueRepository)
{
_issueRepository = issueRepository;
}
public async Task DoItAsync(Guid milesoneId)
{
var queryable = await _issueRepository.GetQueryableAsync();
var issues = AsyncExecuter.ToListAsync(
queryable.Where(new InActiveIssueSpecification()
.Add(new MilestoneSpecification(milestoneId))
.ToExpression()
)
);
}
}
示例中使用 Add 扩展方法组合规约,还有更多的扩展方法,比如:Or(...) AndNot(...)。
学习帮助
围绕DDD和ABP Framework两个核心技术,后面还会陆续发布核心构件实现、综合案例实现系列文章,敬请关注!
ABP Framework 研习社(QQ群:726299208)
专注 ABP Framework 学习及DDD实施经验分享;示例源码、电子书共享,欢迎加入!

基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则的更多相关文章
- 基于ABP落地领域驱动设计-00.目录和小结
<实现领域驱动设计> -- 基于 ABP Framework 实现领域驱动设计实用指南 翻译缘由 自 ABP vNext 1.0 开始学习和使用该框架,被其优雅的设计和实现吸引,适逢 AB ...
- 基于ABP落地领域驱动设计-04.领域服务和应用服务的最佳实践和原则
目录 系列文章 领域服务 应用服务 学习帮助 系列文章 基于ABP落地领域驱动设计-00.目录和前言 基于ABP落地领域驱动设计-01.全景图 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践 ...
- 基于ABP落地领域驱动设计-05.实体创建和更新最佳实践
目录 系列文章 数据传输对象 输入DTO最佳实践 不要在输入DTO中定义不使用的属性 不要重用输入DTO 输入DTO中验证逻辑 输出DTO最佳实践 对象映射 学习帮助 系列文章 基于ABP落地领域驱动 ...
- 基于ABP落地领域驱动设计-06.正确区分领域逻辑和应用逻辑
目录 系列文章 领域逻辑和应用逻辑 多应用层 示例:正确区分应用逻辑和领域逻辑 学习帮助 系列文章 基于ABP落地领域驱动设计-00.目录和前言 基于ABP落地领域驱动设计-01.全景图 基于ABP落 ...
- 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则
目录 前言 聚合 聚合和聚合根原则 包含业务原则 单个单元原则 事务边界原则 可序列化原则 聚合和聚合根最佳实践 只通过ID引用其他聚合 用于 EF Core 和 关系型数据库 保持聚合根足够小 聚合 ...
- 基于ABP落地领域驱动设计-01.全景图
什么是领域驱动设计? 领域驱动设计(简称:DDD)是一种针对复杂需求的软件开发方法.将软件实现与不断发展的模型联系起来,专注于核心领域逻辑,而不是基础设施细节.DDD适用于复杂领域和大规模应用,而不是 ...
- DDD领域驱动设计:仓储
1 前置阅读 在阅读本文章之前,你可以先阅读: 什么是DDD DDD的实体.值对象.聚合根的基类和接口:设计与实现 2 什么是仓储? 仓储封装了基础设施来提供查询和持久化聚合操作. 它们集中提供常见的 ...
- 领域驱动设计(DDD)部分核心概念的个人理解
领域驱动设计(DDD)是一种基于模型驱动的软件设计方式.它以领域为核心,分析领域中的问题,通过建立一个领域模型来有效的解决领域中的核心的复杂问题.Eric Ivans为领域驱动设计提出了大量的最佳实践 ...
- 领域驱动设计(DDD)部分核心概念的个人理解(转)
领域驱动设计(DDD)是一种基于模型驱动的软件设计方式.它以领域为核心,分析领域中的问题,通过建立一个领域模型来有效的解决领域中的核心的复杂问题.Eric Ivans为领域驱动设计提出了大量的最佳实践 ...
随机推荐
- MySQL8.0大表秒加字段,是真的吗?
前言: 很早就听说 MySQL8.0 支持快速加列,可以实现大表秒级加字段.笔者自己本地也有8.0环境,但一直未进行测试.本篇文章我们就一起来看下 MySQL8.0 快速加列到底要如何操作. 1.了解 ...
- 在/etc/profile下配置java的环境变量
在/etc/profile下配置java的环境变量 原创 Java 作者:xiaoyan5686670 时间:2016-01-18 14:30:28 6152 0 以root用户编辑:#vi /e ...
- Linux_搭建NFS服务(基础)
[RHEL8]-NFSserver :[Centos7]-NFSclient !!!测试环境我们首关闭防火墙和selinux(NFSserver和NFSclient都需要) [root@localho ...
- KVM虚拟化存储管理(4)
一.KVM存储虚拟化介绍 KVM 的存储虚拟化是通过存储池(Storage Pool)和卷(Volume)来管理的. Storage Pool 是宿主机上可以看到的一片存储空间,可以是多种型: Vol ...
- C# 技术体系简介
C# 语言 .Net Framwork .NET Core winform界面编程 WPF开发经验,熟悉C/S架构产品开发及架构和设计 DevExpress界面框架(其实就是基于微软的 Winform ...
- 3.21-22 od、tee
3.21 od:按不同进制显示文件 od命令用于输出文件的八进制.十六进制或者其他格式编码的字节,通常用于显示或查看文件中不能直接显示在终端的字符. -A 地址进制 按指定的进制 ...
- RIP OSPF 等路由协议属于计算机网络分层中的哪一层
RIP基于UDP,BGP基于TCP,OSPF EGP基于IP 在TCP/IP协议栈中定义的路由协议用于发现和维护前往目的地的最短路径.可以认为它们不属于网络层协议(注意,是用based on,而不是实 ...
- flink Checkpoint优化
一.设置最小时间间隔 当flink应用开启Checkpoint功能,并配置Checkpoint时间间隔,应用中就会根据指定的时间间隔周期性地对应用进行Checkpoint操作.默认情况下Checkpo ...
- Python数模笔记-StatsModels 统计回归(1)简介
1.关于 StatsModels statsmodels(http://www.statsmodels.org)是一个Python库,用于拟合多种统计模型,执行统计测试以及数据探索和可视化. 2.文档 ...
- linux上传启动项目命令
使用Xshell 或其他远程链接登录工具登录服务器后 1.切换用户到root: sudo -i 账户密码 注意:可直接将jar包放入root用户目录下,避免有可能因为服务器文件夹权限设置导致在指定文件 ...