dotNET兄弟会

专注.Net开源技术及跨平台开发!致力于构建完善的.Net开放技术文库!为.Net爱好者提供学习交流家园!

公众号

围绕DDDABP Framework两个核心技术,后面还会陆续发布核心构件实现综合案例实现系列文章,敬请关注! ABP Framework 研习社(QQ群:726299208) ABP Framework 学习及实施DDD经验分享;示例源码、电子书共享,欢迎加入!

系列文章

基于ABP落地领域驱动设计-01.全景图基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则

仓储

仓储(接口)是一组集合的接口,被领域层和应用层用来访问数据持久化系统(数据库),以读写业务对象,业务对象通常是聚合。

仓储的通用原则

•在领域层中定义仓储接口,在基础层中实现仓储接口(比如: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(...)

学习帮助

围绕DDDABP Framework两个核心技术,后面还会陆续发布核心构件实现综合案例实现系列文章,敬请关注!

ABP Framework 研习社(QQ群:726299208) 专注 ABP Framework 学习及DDD实施经验分享;示例源码、电子书共享,欢迎加入!

基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则相关推荐

  1. 基于ABP落地领域驱动设计-06.正确区分领域逻辑和应用逻辑

    系列文章 基于ABP落地领域驱动设计-01.全景图 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则 基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则 基于ABP落地领域驱动设 ...

  2. 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则

    前言 上一篇 基于ABP落地领域驱动设计-01.全景图 概述了DDD理论和对应的解决方案.项目组成.项目引用关系,以及基于ABP落地DDD的通用原则.从这本篇开始,会更加深入地介绍在基于 ABP Fr ...

  3. 基于ABP落地领域驱动设计-01.全景图

    什么是领域驱动? 领域驱动设计(简称:DDD)是一种针对复杂需求的软件开发方法.将软件实现与不断发展的模型联系起来,专注于核心领域逻辑,而不是基础设施细节.DDD适用于复杂领域和大规模应用,而不是简单 ...

  4. 基于ABP落地领域驱动设计-05.实体创建和更新最佳实践

    围绕DDD和ABP Framework两个核心技术,后面还会陆续发布核心构件实现.综合案例实现系列文章,敬请关注! ABP Framework 研习社(QQ群:726299208) ABP Frame ...

  5. 基于ABP落地领域驱动设计-04.领域服务和应用服务的最佳实践和原则

    围绕DDD和ABP Framework两个核心技术,后面还会陆续发布核心构件实现.综合案例实现系列文章,敬请关注! ABP Framework 研习社(QQ群:726299208) ABP Frame ...

  6. 基于花季A传媒ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则

    ​ DDD和ABP Framework两个中心技术,后边还会陆续发布中心构件完成.归纳事例完成系列文章,敬请关注! ABP Framework 学习及实施DDD经历分享:示例源码.电子书同享,欢迎加入 ...

  7. 领域驱动设计和业务建模的最佳实现模式

    图片来源:pexels.com 做了这么多年项目,不知道你有没有发现一个有趣的现象:有时候面对同一个问题,当我们对它的定义不同,往往最终解决方案的差异也会非常大. 拿我司之前的一个需求来说,客户要求将 ...

  8. 基于DDD(领域驱动设计)的微服务设计实例

    目录 一.战略设计: 1.产品愿景 2.场景分析 3.领域建模 1)提取领域对象 2)构建聚合 3)划分界限上下文 4.微服务拆分 二.战术设计 1.分析微服务领域对象 1)服务识别和设计 2)聚合内 ...

  9. 领域驱动设计(DDD)部分核心概念的个人理解

    领域驱动设计(DDD)是一种软件设计的思考方式.它以领域为核心,分析领域中的问题,通过建立一个领域模型来有效的解决领域中的核心的复杂问题.Eric Ivans为领域驱动设计提出了大量的最佳实践和经验技 ...

最新文章

  1. 一次对语音技术的彻底批判
  2. C++函数模板(二)引用
  3. 介绍MyBatis代码生成网站(四) --- 继承基类
  4. 正则表达式校验例子[原创]
  5. swing的jlist的值怎么获取_彻底解决安卓/IOS获取蓝牙ID不一致的问题
  6. 最小公倍数与最大公约数的研究
  7. Allegro PCB 封装库
  8. 为什么网站打得开,却ping不通, 网站却打得开
  9. 江苏大学京江学院计算机,江苏大学京江学院
  10. SqlServer的填充因子
  11. 神州计算机u盘启动,神州电脑如何使用U盘装系统
  12. 深度解析B端设计规范如何落地?
  13. 日常运维工作的知识体系
  14. 基于Cookie-Editor与curl实现跨设备的文件下载
  15. leetcode/字符串交织,s1和s2字符串交织组成s3
  16. 网络分布式文件系统-MFS搭建以及部署详解
  17. 谷歌今遭遇史上至暗时刻,美司法部正式提起反垄断诉讼!22年前微软曾有此劫-1
  18. stm32f103c8t6介绍
  19. Echarts常用组件
  20. linux调试器——gdb

热门文章

  1. 数据分析和数据挖掘的理论研究必要性
  2. 环境在c盘_如何给女朋友解释为什么 Windows 上面的软件都把自己安装在 C 盘
  3. 单例模式--工厂模式
  4. socket编程学习笔记
  5. Linux的学习思路
  6. 第六次作业—例行报告
  7. PowerDesigner新建模板
  8. Java设计模式(8)组合模式(Composite模式)
  9. 解题报告 树形图计数
  10. 如何删除Apple Music中的连接功能