1

基于ABP实现DDD--仓储实践 - 阿升1990

 1 year ago
source link: https://www.cnblogs.com/shengshengwang/p/16492367.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

  由于软件系统中可能有着不同的数据库,不同的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 Expression<Func<Issue, bool>> 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<Issue> GetIssuesAsync(ISpecification<Issue> 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<Issue, Guid> _issueRepository;
    
    // 构造函数
    public IssueAppService(IRepository<Issue, Guid> 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 Expression<Func<Issue, bool>> ToExpression()
    {
        return x => x.MilestoneId == MilestoneId;
    }
}  

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

public class IssueAppService : ApplicationService, IIssueAppService
{
    private readonly IRepository<Issue, Guid> _issueRepository;
    
    // 构造函数
    public IssueAppService(IRepository<Issue, Guid> 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)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK