圍繞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實施經驗分享;示例原始碼、電子書共享,歡迎加入!