• 基于ABP实现DDD--仓储实践


      由于软件系统中可能有着不同的数据库,不同的ORM,仓储思想的本质是解耦它们。在ABP中具体的实现仓储接口定义在领域层,实现在基础设施层。仓储接口被领域层(比如领域服务)和应用层用来访问数据库,操作聚合根,聚合根就是业务单元。这篇文章主要分析怎么通过规约将业务逻辑从仓储实现中剥离出来,从而让仓储专注于数据处理。

    一.业务需求

    还是以Issue聚合根为例,假如有个业务规则是:判断是否是未激活的Issue,条件是打开状态、未分配给任何人、创建超过30天、最近30天没有评论。Issue聚合根如下:

    二.在仓储中实现业务逻辑

    该业务规则在基础设施层中实现如下:

    namespace IssueTracking.Issues
    {
        public class EfCoreIssueRepository : EfCoreRepository<IssueTrackingDbContext, IssueTracking, Guid>, IIssueRepository
        {
            // 构造函数
            public EfCoreIssueRepository(IDbContextProvider<IssueTrackingDbContext> dbContextProvider) : base(dbContextProvider)
            {
            }
            
            // 判断是否是未激活的Issue
            public async Task<List<Issue>> GetInActiveIssuesAsync()
            {
                var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30));
                
                var dbSet = await GetDbSetAsync();
                return await dbSet.Where(i =>
                    // 打开状态
                    !i.IsClosed &&
                    // 无分配人
                    i.AssignedUserId == null &&
                    // 创建时间在30天前
                    i.CreationTime < daysAgo30 &&
                    // 没有评论或最后一次评论在30天前
                    (i.LastCommentTime == null || i.LastCommentTime < daysAgo30)
                ).toListAsync();
            }
        }
    }
    
    折叠

    根据DDD中仓储的实践原则,肯定是不能将业务逻辑放在仓储实现中的,接下来使用规约的方式来解决这个问题。

    三.使用规约实现业务逻辑

    规约就是一种约定,规范来讲:规约是一个命名的、可重用的、可组合的和可测试的类,用于根据业务规则来过滤领域对象。通过ABP中的Specification规约基类创建规约类,将判断Issue是否激活这个业务规则实现为一个规约类如下:

    namespace IssueTracking.Issues
    {
        public class InActiveIssueSpecification : Specification<Issue>
        {
            public override Expressionbool>> ToExpression()
            {
                var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30));
                return i =>
                    // 打开状态
                    !i.IsClosed &&
                    // 无分配人
                    i.AssignedUserId == null &&
                    // 创建时间在30天前
                    i.CreationTime < daysAgo30 &&
                    // 没有评论或最后一次评论在30天前
                    (i.LastCommentTime == null || i.LastCommentTime < daysAgo30)
            }
        }
    }
    

    接下来讲解在Issue实体和EfCoreIssueRepository类中如何使用InActiveIssueSpecification规约。

    四.在实体中使用规约

    规约是根据业务规则来过滤领域对象,Issue聚合根中的IsInActive()方法实现如下:

    public class Issue : AggregateRoot<Guid>, IHasCreationTime
    {
        public bool IsClosed { get; private set; }
        public Guid? AssignedUserId { get; private set; }
        public DateTime CreateTime { get; private set; }
        public DateTime? LastCommentTime { get; private set; }
        
        // 判断Issue是否未激活
        public bool IsInActive()
        {
            return new InActiveIssueSpecification().IsSatisfiedBy(this);
        }
    }
    

    创建一个InActiveIssueSpecification实例,使用它的IsSatisfiedBy()来进行规约验证。

    五.在仓储中使用规约

    领域层中的(自定义)仓储接口如下,GetIssuesAsync()接收一个规约对象参数:

    public interface IIssueRepository : IRepository<Issue, Guid>
    {
        Task<List GetIssuesAsync(ISpecification spec);
    }  
    

    基础设施层中的(自定义)仓储实现如下:

    public class EfCoreIssueRepository: EfCoreRepository<IssueTrackingDbContext, EfCoreIssueRepository, Guid>, IIssueRepository
        {
            // 构造函数
            public EfCoreIssueRepository(IDbContextProvider<IssueTrackingDbContext> dbContextProvider) : base(dbContextProvider)
            {
            }
            
            public async Task<List<Issue>> GetIssuesAsync(ISpecification<Issue> spec)
            {
                var dbSet = await GetDbSetAsync();
                // 通过表达式实现Issue实体过滤
                return await dbSet.Where(spec.ToExpression()).ToListAsync();
            }
        }
    }  
    

    应用层使用规约如下,本质上就是新建一个规约实例,然后作为GetIssuesAsync()的参数:

    public class IssueAppService : ApplicationService, IIssueAppService
    {
        private readonly IIssueRepository _issueRepository;
        
        // 构造函数
        public IssueAppService(IIssueRepository issueRepository)
        {
            _issueRepository = issueRepository;
        }
    
        public async Task DoItAsync()
        {
            // 在应用层通过仓储使用规约来过滤实体
            var issues = await _issueRepository.GetIssuesAsync(new InActiveIssueSpecification());
        }
    }  
    

    六.在应用层中通过默认仓储来使用规约

    上面是在应用层中通过自定义仓储来使用规约的,接下来讲解在应用层中通过默认仓储来使用规约:

    public class IssueAppService : ApplicationService, IIssueAppService
    {
        private readonly IRepository _issueRepository;
        
        // 构造函数
        public IssueAppService(IRepository issueRepository)
        {
            _issueRepository = issueRepository;
        }
    
        public async Task DoItAsync()
        {
            var queryable = await _issueRepository.GetQueryableAsync();
            // 简单理解,queryable就是查询出来的实体,然后根据规约进行过滤
            var issues = AsyncExecuter.ToListAsync(queryable.Where(new InActiveIssueSpecification()));
        }
    }  
    

    说明:AsyncExecuter是ABP提供的一个工具类,用于使用异步LINQ拓展方法,而不依赖于EF Core NuGet包。

    七.组合规约的使用

    规约是可组合使用的,这样就变的很强大。比如,再定义一个规约,当Issue是指定里程碑是返回True。定义新的规约如下:

    public class MilestoneSpecification : Specification<Issue>
    {
        public Guid MilestoneId { get; }
        
        // 构造函数
        public MilestoneSpecification(Guid milestoneId)
        {
            MilestoneId = milestoneId;
        }
        
        public override Expressionbool>> ToExpression()
        {
            return x => x.MilestoneId == MilestoneId;
        }
    }  
    

    如果和上面定义的InActiveIssueSpecification规约组合,就可以实现业务逻辑:获取指定里程碑中未激活的Issue:

    public class IssueAppService : ApplicationService, IIssueAppService
    {
        private readonly IRepository _issueRepository;
        
        // 构造函数
        public IssueAppService(IRepository issueRepository)
        {
            _issueRepository = issueRepository;
        }
    
        public async Task DoItAsync(Guid milestoneId)
        {
            var queryable = await _issueRepository.GetQueryableAsync();
            // 组合规约的使用方法,除了Add扩展方法,还有Or()、And()、Not()等方法
            var issues = AsyncExecuter.ToListAsync(
                queryable.Where(new InActiveIssueSpecification()
                    .Add(new MilestoneSpecification(milestoneId))
                    .ToExpression()
                )
            );
        }
    }  
    

    参考文献:
    [1]基于ABP Framework实现领域驱动设计:https://url39.ctfile.com/f/2501739-616007877-f3e258?p=2096 (访问密码: 2096)

  • 相关阅读:
    你不知道的SQL语言数据库原理
    Mysql-解决Truncated incorrect DOUBLE value xxx
    4.新建模块和代码生成
    07-app端文章搜索-黑马头条
    机器学习算法 —— 1. K近邻算法
    线程的创建方式
    Hantek6022BE 虚拟示波器
    Vue脚手架配置代理
    2022高教社杯数学建模国赛C题思路代码实现
    深夜测评:讯飞星火大模型vs FuncGPT (慧函数),到底哪家强?
  • 原文地址:https://www.cnblogs.com/shengshengwang/p/16492367.html