圍繞DDD和ABP 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(...)
。
學習幫助
圍繞DDD和ABP Framework兩個核心技術,後面還會陸續釋出核心構件實現、綜合案例實現系列文章,敬請關注!
ABP Framework 研習社(QQ群:726299208)
專注 ABP Framework 學習及DDD實施經驗分享;示例原始碼、電子書共享,歡迎加入!