實現領域驅動設計

2022-06-23 18:02:31

儲存庫

Repository 是一個類似於集合的介面,領域層和應用程式層使用它來存取資料永續性系統(資料庫),以讀寫業務物件(通常是聚合)

常見的儲存庫原則是:

  • 在領域層定義一個儲存庫介面(因為它被用於領域層和應用層),在基礎設施層實現(啟動模板中的EntityFrameworkCore專案)
  • 不要在儲存庫中包含業務邏輯。
  • 儲存庫介面應該是獨立於資料庫提供者/ ORM的。例如,不要從儲存庫方法返回DbSet。DbSet是 EF Core 提供的一個物件
  • 為聚合根建立儲存庫,而不是為所有實體。因為,子集合實體(聚合的)應該通過聚合根存取

不要在儲存庫中包含領域邏輯

雖然這個規則在一開始看起來很明顯,但是很容易將業務邏輯洩露到儲存庫中

範例:從儲存庫中獲取不活躍的問題

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

IIssueRepository 擴充套件了標準 IRepository<...> 介面,新增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; }
}

(程式碼只顯示了本例所需的屬性)

規則規定儲存庫不應該知道業務規則。這裡的問題是 「什麼是不活躍的問題? 」它是業務規則定義嗎?」

讓我們看看實現來理解它:

public class EfCoreIssueRepository : 
    EfCoreRepository<IssueTrackingDbContext, Issue, Guid>
    IIssueRepository
{
    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();
    }
}

(使用EF Core實現。檢視 EF Core整合檔案,瞭解如何使用 EF Core 建立自定義儲存庫。)

當我們檢查 GetInActiveIssuesAsync 的實現時,我們看到了一個業務規則,它給出了不活躍的問題的定義:該問題應該是開放的,沒有分配給任何人,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 CreationTime { 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);
    }
}

我們必須複製/貼上/修改程式碼。如果活動性的定義改變了呢?我們不應該忘記更新這兩個地方。這是業務邏輯的重複,這是非常危險的

這個問題的一個很好的解決方案是規範模式!

規範

規範是一個命名的、可重用的、可組合的和可測試的類,用於基於業務規則篩選領域物件

ABP框架提供了必要的基礎設施來輕鬆地建立規範類並在應用程式程式碼中使用它們。讓我們將不活躍的問題過濾器實現為一個規範類:

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);
    }
}

Specification<T> 基礎類別通過定義表示式簡化了建立規範類的工作。只是將表示式從儲存庫移到這裡
現在我們就可以在 Issue 實體和 EfCoreIssueRepository 類中複用 InActiveIssueSpecification 了

在實體中使用規範

Specification 類提供了一個 IsSatisfiedBy 方法,如果給定的物件(實體)滿足規範,該方法返回true。我們可以重寫這個 Issue。IsInActive 方法如下所示:

public class Issue : AggregateRoot<Guid>, IHasCreationTime
{
    public bool IsInActive()
    {
       return new InActiveIssueSpecification().IsSatisfiedBy(this);
    }
}

在儲存庫中使用規範

首先,從儲存庫介面開始:

public interface IIssueRepository : IRepository<Issue, Guid>
{
    Task<List<Issue>> GetIssuesAsync(ISpecification<Issue> spec);
}
  • 將 GetInActiveIssuesAsync 重新命名為簡單的 GetIssuesAsync, 並接受一個規範物件
  • 由於規範(過濾器)已經從儲存庫中移出,我們不再需要建立不同的方法來獲得不同條件下的問題(比如: GetAssignedIssues(...) , GetLockedIssues(...) 等等)

更新後的儲存庫實現可以像這樣:

public class EfCoreIssueRepository : 
    EfCoreRepository<IssueTrackingDbContext, Issue, Guid>
    IIssueRepository
{
    public async Task<List<Issue>> GetIssuesAsync(ISpecification<Issue> spec)
    {
        var dbSet = await GetDbSetAsync();
        return await dbSet
            .Where(spec.ToExpression())
            .ToListAsync();
    }
}

因為ToExpression()方法返回一個表示式,所以它可以直接傳遞給Where方法來過濾實體

  • 最終,我們做到了業務邏輯的程式碼複用,消除了安全隱患

使用預設的儲存庫

實際上,您不必建立自定義儲存庫才能使用規範。標準的IRepository已經擴充套件了IQueryable,所以你可以在上面使用標準的LINQ擴充套件方法:

public class IssueAppService : ApplicationService, IIssueAppService
{
    public async Task<List<Issue>> GetInActiveIssuesAsync()
    {
        var queryable = await _issueRepository.GetQueryableAsync();
        var issues = await AsyncExecuter.ToListAsync(
            queryable.Where(new InActiveIssueSpecification())
        );
    }
}

AsyncExecuter 是ABP框架提供的一個實用工具,用於使用非同步LINQ擴充套件方法(如這裡的ToListAsync),而不依賴於EF Core NuGet包。有關更多資訊,請參閱 Repositories檔案

組合規範

規範的一個強大的方面是它們是可組合的。假設我們有另一個規範,它只在問題位於里程碑時返回true

public class MilestoneSpecification : Specification<Issue>
{
    public Guid MilestoneId { get; }
    public override Expression<Func<Issue,bool>> ToExpression()
    {
        return i => i.MilestoneId == MilestoneId;
    }
}

本規範是引數化的,與 InActiveIssueSpecification 有所不同。我們可以結合這兩個規範來獲得特定里程碑中的非活躍問題列表

public class IssueAppService : ApplicationService, IIssueAppService
{
    public async Task<List<Issue>> GetInActiveIssuesWithinMilestoneAsync(Guid milestoneId)
    {
        var queryable = await _issueRepository.GetQueryableAsync();
        var issues = await AsyncExecuter.ToListAsync(
            queryable.Where(
                new InActiveIssueSpecification()
                .And(new MilestoneSpecification(milestoneId))
                .ToExpression()
            )
        );
    }
}

上面的範例使用And擴充套件方法來組合這些規範。還有更多的組合方法可用,比如 Or(…) 和 AndNot(…)

有關ABP框架提供的規範基礎架構的更多細節,請參閱 規範檔案