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