基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则
dotNET兄弟會?
專注.Net開源技術及跨平臺開發!致力于構建完善的.Net開放技術文庫!為.Net愛好者提供學習交流家園!
公眾號 ?
圍繞DDD和ABP Framework兩個核心技術,后面還會陸續發布核心構件實現、綜合案例實現系列文章,敬請關注!?ABP Framework 研習社(QQ群:726299208)?ABP Framework 學習及實施DDD經驗分享;示例源碼、電子書共享,歡迎加入!
系列文章
基于ABP落地領域驅動設計-01.全景圖基于ABP落地領域驅動設計-02.聚合和聚合根的最佳實踐和原則
倉儲
倉儲(接口)是一組集合的接口,被領域層和應用層用來訪問數據持久化系統(數據庫),以讀寫業務對象,業務對象通常是聚合。
倉儲的通用原則
?在領域層中定義倉儲接口,在基礎層中實現倉儲接口(比如: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實施經驗分享;示例源碼、電子書共享,歡迎加入!
總結
以上是生活随笔為你收集整理的基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于ABP落地领域驱动设计-06.正确区
- 下一篇: 基于ABP落地领域驱动设计-04.领域服