热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:基于ABP落地领域驱动设计03.仓储和规约最佳实践和原则

篇首语:本文由编程笔记#小编为大家整理,主要介绍了基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则相关的知识,希望对你有一定的参考价值。









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
{
Task> GetInActiveIssuesAsync();
}
}

IIssueRepository 继承 IRepository 接口,添加了 GetInActiveIssuesAsync() 方法。与之对应的聚合根类型是 Issue 类:


public class Issue:AggregateRoot,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,
IIssueRepository
{
public EfCoreIssueRepository(
IDbContextProvider dbContextProvider
):base(dbContextProvider)
{}
public async Task> 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 //没有评论或最后一次评论在30天前
(i.LastCommentTime == null || i.LastCommentTime ).ToListAsync();
}
}
}

在 GetInActiveIssueAsynce 实现方法中,对于未激活的Issue 这条业务规则,需要满足条件:打开状态、未分配给任何人、创建超过30天、最近30天没有评论。


如果我们将业务规则隐含在仓储中,当我们需要重复使用这个业务逻辑时,问题就出现了。


举个例子,在 Issue 实体中希望添加一个方法 bool IsInActive(),用于检测 Issue 是否未激活状态。


看看如何实现:


public class Issue:AggregateRoot,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 //无评论或最后一次评论在30天前
(LastCommentTime == null || LastCommentTime }
}

我们不得不复制、粘贴、修改代码。如果对未激活的Issue 规则改变了怎么办?我们应该记得同时更新这两个地方。这是业务逻辑重复,代码的坏味道,是相当危险的。


这个问题的一个很好的解决方案就是规约


规约


规约是一个命名的、可重用的可组合的和可测试的类,用于根据业务规则过滤领域对象


ABP框架提供了必要的基础设施,以轻松创建规约并在你的应用程序代码中使用。让我们把 inactive Issue 非活动问题业务规则实现为一个规约类


using System;
using System.Linq.Expressions;
using Volo.Abp.Specifications;
namespace IssueTracking.Issues
{
public class InActiveIssueSpecification:Specification
{
public override Expression> ToExpression()
{
var daysAgo30=DateTime.Now.Subtract(TimeSpan.FromDays(30));
return i =>
//打开状态
!i.IsClosed &&
//无分配人
i.AssingedUserId ==null &&
//创建时间超过30天
i.CreationTime //没有评论或最后评论超过30天
(i.LastCommentTime == null || i.LastCommentTime }
}
}

Specification 基类可以帮助我们简单地创建规约类,我们可以将仓储中的表达式移到规约中。


现在,可以在 Issue 实体和 EfCoreIssueRepository 类中使用 InActiveIssueSpecification 规约。


在实体中使用规约


Specification类提供了一个IsSatisfiedBy方法,如果给定的对象(实体)满足该规范,则返回true。我们可以重新编写Issue.IsInActive方法,如下所示:


public class Issue:AggregateRoot,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
{
Task> GetIssuesAsync(ISpecification spec);
}

将方法名 GetInActiveIssuesAsync 改为 GetIssuesAsync (命名更加简洁),接收一个规约对象参数。将规约判断的代码逻辑从仓储中移出之后,我们不再需要定义不同的方法来获取不同条件下的Issue,比如:GetAssignedIssues(...) 获取已有分配人的问题列表,GetLockedIssues(...) 获取已锁定问题列表 等。


修改仓储的实现:


public class EfCoreIssueRepository:
EfCoreRepository,
IIssueRepository
{
public EfCoreIssueRepository(
IDbContextProvider dbContextProvider
):base(dbContextProvider)
{}
public async Task> GetIssuesAsync(ISpecification 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 _issueRepository;
public IssueAppService (IRepository 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
{
public Guid MilestoneId{get;}
public MilestoneSpecification (Guid milestoneId)
{
MilestoneId = milestoneId;
}
public override Expression> ToExpression()
{
return i => i.MilestoneId == MilestoneId;
}
}

我们新定义了一个新的参数化规约,和前面定义 InActiveIssueSpecification 不同。那么如何组合两个规约,获取指定里程碑中未激活的 Issue(问题)呢?


public class IssueAppServie : ApplciationService,IIssueAppService
{
private readonly IRepository _issueRepository;
public IssueAppService (IRepository 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实施经验分享;示例源码、电子书共享,欢迎加入!





推荐阅读
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 本文介绍了使用PHP实现断点续传乱序合并文件的方法和源码。由于网络原因,文件需要分割成多个部分发送,因此无法按顺序接收。文章中提供了merge2.php的源码,通过使用shuffle函数打乱文件读取顺序,实现了乱序合并文件的功能。同时,还介绍了filesize、glob、unlink、fopen等相关函数的使用。阅读本文可以了解如何使用PHP实现断点续传乱序合并文件的具体步骤。 ... [详细]
  • 本文详细介绍了Spring的JdbcTemplate的使用方法,包括执行存储过程、存储函数的call()方法,执行任何SQL语句的execute()方法,单个更新和批量更新的update()和batchUpdate()方法,以及单查和列表查询的query()和queryForXXX()方法。提供了经过测试的API供使用。 ... [详细]
  • 第四章高阶函数(参数传递、高阶函数、lambda表达式)(python进阶)的讲解和应用
    本文主要讲解了第四章高阶函数(参数传递、高阶函数、lambda表达式)的相关知识,包括函数参数传递机制和赋值机制、引用传递的概念和应用、默认参数的定义和使用等内容。同时介绍了高阶函数和lambda表达式的概念,并给出了一些实例代码进行演示。对于想要进一步提升python编程能力的读者来说,本文将是一个不错的学习资料。 ... [详细]
  • 基于词向量计算文本相似度1.测试数据:链接:https:pan.baidu.coms1fXJjcujAmAwTfsuTg2CbWA提取码:f4vx2.实验代码:imp ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • 如何基于ggplot2构建相关系数矩阵热图以及一个友情故事
    本文介绍了如何在rstudio中安装ggplot2,并使用ggplot2构建相关系数矩阵热图。同时,通过一个友情故事,讲述了真爱难觅的故事背后的数据量化和皮尔逊相关系数的概念。故事中的小伙伴们在本科时参加各种考试,其中有些沉迷网络游戏,有些热爱体育,通过他们的故事,展示了不同兴趣和特长对学习和成绩的影响。 ... [详细]
  • Oracle分析函数first_value()和last_value()的用法及原理
    本文介绍了Oracle分析函数first_value()和last_value()的用法和原理,以及在查询销售记录日期和部门中的应用。通过示例和解释,详细说明了first_value()和last_value()的功能和不同之处。同时,对于last_value()的结果出现不一样的情况进行了解释,并提供了理解last_value()默认统计范围的方法。该文对于使用Oracle分析函数的开发人员和数据库管理员具有参考价值。 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • 也就是|小窗_卷积的特征提取与参数计算
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了卷积的特征提取与参数计算相关的知识,希望对你有一定的参考价值。Dense和Conv2D根本区别在于,Den ... [详细]
  • 目录4.1.type数据类型检测 ... [详细]
  • 介绍平常在多线程开发中,总避免不了线程同步。本篇就对net多线程中的锁系统做个简单描述。目录一:lock、Monitor1:基础 ... [详细]
  • request  的上传文件
    前言:注册接口需要上次头像,fiddle抓的接口如图,这个时候就需要用到:files2,举例说明a࿱ ... [详细]
  • MySQL修改表结构操作命令总结【MySQL】
    数据库|mysql教程MySQL,修改表结构命令数据库-mysql教程表的结构如下:错误页面源码,ubuntu电脑自动休眠,爬虫造景视频,rapapiphp,廊坊seo开发lzwm ... [详细]
  • 对hishop 商城 web.config加密,和解密码详细说明 ... [详细]
author-avatar
小菜鸟
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有