系列文章
- 基於ABP落地領域驅動設計-00.目錄和前言
- 基於ABP落地領域驅動設計-01.全景圖
- 基於ABP落地領域驅動設計-02.聚合和聚合根的最佳實踐和原則
- 基於ABP落地領域驅動設計-03.倉儲和規約最佳實踐和原則
- 基於ABP落地領域驅動設計-04.領域服務和應用服務的最佳實踐和原則
- 基於ABP落地領域驅動設計-05.實體創建和更新最佳實踐
- 基於ABP落地領域驅動設計-06.正確區分領域邏輯和應用邏輯
圍繞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實施經驗分享;示例源碼、電子書共享,歡迎加入!