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


    系列文章

    围绕DDDABP 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(...)

    学习帮助

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

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

    记录技术修行中的反思与感悟,以码传心,以软制道,知行合一!
  • 相关阅读:
    【洛谷P1297】单选错位【期望】
    【洛谷P1297】单选错位【期望】
    【POJ1201】Intervals【差分约束】
    【POJ1201】Intervals【差分约束】
    【洛谷P3275】糖果【差分约束】【负环】
    【洛谷P3275】糖果【差分约束】【负环】
    【洛谷P1768】天路【负环】【二分】【数论】
    【洛谷P1768】天路【负环】【二分】【数论】
    【JZOJ4256】平均数【二分】
    【JZOJ4256】平均数【二分】
  • 原文地址:https://www.cnblogs.com/YGYH/p/14931390.html
Copyright © 2020-2023  润新知