基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则
dotNET兄弟會?
專注.Net開源技術(shù)及跨平臺開發(fā)!致力于構(gòu)建完善的.Net開放技術(shù)文庫!為.Net愛好者提供學(xué)習(xí)交流家園!
公眾號 ?
圍繞DDD和ABP Framework兩個核心技術(shù),后面還會陸續(xù)發(fā)布核心構(gòu)件實現(xiàn)、綜合案例實現(xiàn)系列文章,敬請關(guān)注!?ABP Framework 研習(xí)社(QQ群:726299208)?ABP Framework 學(xué)習(xí)及實施DDD經(jīng)驗分享;示例源碼、電子書共享,歡迎加入!
系列文章
基于ABP落地領(lǐng)域驅(qū)動設(shè)計-01.全景圖基于ABP落地領(lǐng)域驅(qū)動設(shè)計-02.聚合和聚合根的最佳實踐和原則
倉儲
倉儲(接口)是一組集合的接口,被領(lǐng)域?qū)雍蛻?yīng)用層用來訪問數(shù)據(jù)持久化系統(tǒng)(數(shù)據(jù)庫),以讀寫業(yè)務(wù)對象,業(yè)務(wù)對象通常是聚合。
倉儲的通用原則
?在領(lǐng)域?qū)又卸x倉儲接口,在基礎(chǔ)層中實現(xiàn)倉儲接口(比如:EntityFrameworkCore項目或MongoDB項目)?倉儲不包含業(yè)務(wù)邏輯,專注數(shù)據(jù)處理。?倉儲接口應(yīng)該保持?數(shù)據(jù)提供程序/ORM 獨立性。舉個例子,倉儲接口定義的方法不能返回?DbSet?對象,因為該對象由 EF Core 提供,如果使用?MongoDB?數(shù)據(jù)庫則無法實現(xiàn)該接口。?為聚合根創(chuàng)建對應(yīng)倉儲,而不是所有實體。因為子集合實體(聚合)應(yīng)該通過聚合根訪問。
倉儲中不包含領(lǐng)域邏輯
雖然這個規(guī)則一開始看起來很好理解,但在實際開發(fā)過程中,很容易在不經(jīng)意間將業(yè)務(wù)邏輯放到倉儲中。
示例:從倉儲中獲取?inactive?狀態(tài)的 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()?方法。與之對應(yīng)的聚合根類型是?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;} }規(guī)則要求我們:倉儲不應(yīng)該知道業(yè)務(wù)規(guī)則,那么問題來了:什么是 inactive Issue(未激活的問題)?這是業(yè)務(wù)規(guī)則。
為了更好地理解,我們繼續(xù)看看接口方法的實現(xiàn):
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=>//打開狀態(tài)!i.IsClosed &&//無分配人i.AssingedUserId ==null &&//創(chuàng)建時間在30天前i.CreationTime < daysAgo30 &&//沒有評論或最后一次評論在30天前(i.LastCommentTime == null || i.LastCommentTime < daysAgo30)).ToListAsync();}} }在?GetInActiveIssueAsynce?實現(xiàn)方法中,對于未激活的Issue?這條業(yè)務(wù)規(guī)則,需要滿足條件:打開狀態(tài)、未分配給任何人、創(chuàng)建超過30天、最近30天沒有評論。
如果我們將業(yè)務(wù)規(guī)則隱含在倉儲中,當(dāng)我們需要重復(fù)使用這個業(yè)務(wù)邏輯時,問題就出現(xiàn)了。
舉個例子,在 Issue 實體中希望添加一個方法?bool IsInActive(),用于檢測 Issue 是否未激活狀態(tài)。
看看如何實現(xiàn):
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//打開狀態(tài)!IsClosed &&//無分配人AssignedUserId ==null &&//創(chuàng)建時間在30天前CreationTime < daysAgo30 &&//無評論或最后一次評論在30天前(LastCommentTime == null || LastCommentTime < daysAgo30 );} }我們不得不復(fù)制、粘貼、修改代碼。如果對未激活的Issue 規(guī)則改變了怎么辦?我們應(yīng)該記得同時更新這兩個地方。這是業(yè)務(wù)邏輯重復(fù),代碼的壞味道,是相當(dāng)危險的。
這個問題的一個很好的解決方案就是規(guī)約。
規(guī)約
規(guī)約是一個命名的、可重用的、可組合的和可測試的類,用于根據(jù)業(yè)務(wù)規(guī)則過濾領(lǐng)域?qū)ο?/strong>。
ABP框架提供了必要的基礎(chǔ)設(shè)施,以輕松創(chuàng)建規(guī)約并在你的應(yīng)用程序代碼中使用。讓我們把?inactive Issue?非活動問題業(yè)務(wù)規(guī)則實現(xiàn)為一個規(guī)約類。
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 =>//打開狀態(tài)!i.IsClosed &&//無分配人i.AssingedUserId ==null &&//創(chuàng)建時間超過30天i.CreationTime < daysAgo30 &&//沒有評論或最后評論超過30天(i.LastCommentTime == null || i.LastCommentTime < daysAgo30)}} }Specification<T>?基類可以幫助我們簡單地創(chuàng)建規(guī)約類,我們可以將倉儲中的表達式移到規(guī)約中。
現(xiàn)在,可以在?Issue?實體和?EfCoreIssueRepository?類中使用?InActiveIssueSpecification?規(guī)約。
在實體中使用規(guī)約
Specification類提供了一個IsSatisfiedBy方法,如果給定的對象(實體)滿足該規(guī)范,則返回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);} }創(chuàng)建一個?InActiveIssueSpecification?新實例,使用其?IsSatisfiedBy?方法,進行規(guī)約驗證。
在倉儲中使用規(guī)約
首先,修改倉儲接口:
public interface IIssueRepository:IRepository<Issue,Guid> {Task<List<Issue>> GetIssuesAsync(ISpecification<Issue> spec); }將方法名?GetInActiveIssuesAsync?改為?GetIssuesAsync?(命名更加簡潔),接收一個規(guī)約對象參數(shù)。將規(guī)約判斷的代碼邏輯從倉儲中移出之后,我們不再需要定義不同的方法來獲取不同條件下的Issue,比如:GetAssignedIssues(...)?獲取已有分配人的問題列表,GetLockedIssues(...)?獲取已鎖定問題列表 等。
修改倉儲的實現(xiàn):
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?方法的參數(shù)傳遞,實現(xiàn)實體過濾。
最后,我們將規(guī)約實例,傳遞給?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(););} }默認倉儲
實際上,你不需要創(chuàng)建自定義倉儲就能使用規(guī)約。標(biāo)準(zhǔn)的IRepository?接口已經(jīng)擴展?IQueryable?接口,所以你可以直接使用標(biāo)準(zhǔn)的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 包。
組合規(guī)約
規(guī)范的一個強大的地方是它們是可以組合使用的。假設(shè)我們有另一個規(guī)約,當(dāng)問題 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;} }我們新定義了一個新的參數(shù)化規(guī)約,和前面定義?InActiveIssueSpecification?不同。那么如何組合兩個規(guī)約,獲取指定里程碑中未激活的 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?擴展方法組合規(guī)約,還有更多的擴展方法,比如:Or(...)?AndNot(...)。
學(xué)習(xí)幫助
圍繞DDD和ABP Framework兩個核心技術(shù),后面還會陸續(xù)發(fā)布核心構(gòu)件實現(xiàn)、綜合案例實現(xiàn)系列文章,敬請關(guān)注!
ABP Framework 研習(xí)社(QQ群:726299208)?專注 ABP Framework 學(xué)習(xí)及DDD實施經(jīng)驗分享;示例源碼、電子書共享,歡迎加入!
總結(jié)
以上是生活随笔為你收集整理的基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于ABP落地领域驱动设计-06.正确区
- 下一篇: 基于ABP落地领域驱动设计-04.领域服