基於ABP實現DDD--倉儲實踐

2022-07-19 06:02:11

  由於軟體系統中可能有著不同的資料庫,不同的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)