基於ABP落地領域驅動設計-04.領域服務和應用服務的最佳實踐和原則
系列文章
- 基於ABP落地領域驅動設計-00.目錄和前言
- 基於ABP落地領域驅動設計-01.全景圖
- 基於ABP落地領域驅動設計-02.聚合和聚合根的最佳實踐和原則
- 基於ABP落地領域驅動設計-03.倉儲和規約最佳實踐和原則
- 基於ABP落地領域驅動設計-04.領域服務和應用服務的最佳實踐和原則
- 基於ABP落地領域驅動設計-05.實體建立和更新最佳實踐
- 基於ABP落地領域驅動設計-06.正確區分領域邏輯和應用邏輯
圍繞DDD和ABP Framework兩個核心技術,後面還會陸續釋出核心構件實現、綜合案例實現系列文章,敬請關注!
ABP Framework 研習社(QQ群:726299208)
ABP Framework 學習及實施DDD經驗分享;示例原始碼、電子書共享,歡迎加入!
領域服務
領域服務實現領域邏輯,它:
- 依賴於服務和倉儲。
- 需要多個聚合,以實現單個聚合無法處理的邏輯。
領域服務與領域物件一起使用,其方法可以獲取和返回實體、值物件、原始型別等。然而,它並不獲取/返回DTOs,DTOs屬於應用層。
示例:將問題分配給使用者
回想一下,我們之前是如何實現將問題分配給使用者的
public class Issue:AggregateRoot<Guid> { //.. //問題關聯的使用者ID public Guid? AssignedUserId{get;private set;} //分配方法 public async Task AssignToAsync(AppUser user,IUserIssueService userIssueService) { var openIssueCount = await userIssueService.GetOpenIssueCountAsync(user.Id); if(openIssueCount >=3 ) { throw new BusinessException("IssueTracking:CanNotOpenLockedIssue"); } AssignedUserId=user.Id; } public void CleanAssignment() { AssignedUserId=null; } }
現在,我們將邏輯遷移到領域服務中。首先,修改 Issue 類:
public class Issue:AggregateRoot<Guid>
{
//...
public Guid? AssignedUserId{get;internal set;}
}
- 在聚合中移除
AssignToAsync
方法(因為需要在對應的領域服務中實現該方法。) - 將
AssignedUserId
屬性設定器從私有改為內部internal
,以允許從領域服務中設定它。
接下來,建立一個領域服務 IssueManager
定義方法 AssignToAsync
將指定 Issue
分配給指定使用者。
public class IssueManager:DomainService
{
private readonly IRepository<Issue,Guid> _issueRepository;
public IssueManager(IRepository<Issue,Guid> issueRepository)
{
_issueRepository=issueRepository;
}
public async Task AssignToAsync(Issue issue,AppUser user)
{
//獲取關聯使用者處於開啟狀態問題的數量
var openIssueCount=await _issueRepository.CountAsync(
i=>i.AssingedUserId==user.Id && !i.IsClosed
);
//超過3個,則丟擲異常
if(openIssueCount>=3)
{
throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit");
}
issue.AssignedUserId=user.Id;
}
}
IssueManager
在建構函式中注入需要的倉儲,用於查詢分配給使用者處於開啟狀態的Issue。
建議使用
Manager
字尾命名來命名領域服務。
這種設計的唯一問題是:Issue.AssignedUserId
現在是 public
,可以在任何外部類中設定。然而,它不應該是公共的,訪問範圍應該是程式集內部internal
,只有在同一個程式集(IssueTracking.Domain
)專案中才可以呼叫。
這個例子的解決方案就是如此,我們認為這很合理:
- 領域層開發者在使用 IssueManager 時,已經熟知領域規則。
- 應用層開發者強制使用 IssueManager,因此無法直接修改實體。
以上我們展示了將問題分配給使用者的兩種實現方式,兩種方式權衡之下,我們更加推薦當業務邏輯需要與外部服務協同工作時,建立領域服務。
如果沒有一個充分的理由,我們認為沒有必要去為領域服務建立介面,比如:為
IssueManager
建立IIssueManger
介面。
應用服務
應用服務是無狀態服務,實現應用程式用例。一個應用服務通常使用領域物件實現用例,獲取或返回資料傳輸物件DTOs,被展示層呼叫。
應用服務通用原則:
- 實現特定用例的應用邏輯,不能在應用服務中實現領域邏輯(需要理清應用邏輯和領域邏輯二者的區別)。
- 應用服務方法不能返回實體,因為這樣會打破領域層的封裝性,始終只返回DTO。
示例:分配問題給使用者
using System;
using System.Threading.Tasks;
using IssueTracking.Users;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace IssueTracking.Issues
{
public class IssueAppService :ApplicationService.IIssueAppService
{
private readonly IssueManager _issueManager;
private readonly IRepository<Issue,Guid> _issueRepository;
private readonly IRepository<AppUser,Guid> _userRepository;
public IssueAppService(
IssueManager issueManager,
IRepository<Issue,Guid> issueRepository,
IRepository<AppUser,Guid> userRepository
)
{
_issueManager=issueManager;
_issueRepository=issueRepository;
_userRepository=userRepository;
}
[Authorize]
public async Task AssignAsync(IssueAssignDto input)
{
var issue=await _issueRepository.GetAsync(input.IssueId);
var user=await _userRepository.GetAsync(inpu.UserId);
await _issueManager.AssignToAsync(issue,user);
await _issueRepository.UpdateAsync(issue);//沒有對issue做任何修改,為什麼要更新?在IssueManager中進行了狀態修改。
}
}
}
一個應用服務方法通常有三個步驟:
- 從資料庫獲取關聯的領域物件
- 使用領域物件(領域服務、實體等)執行業務邏輯
- 在資料庫中更新實體(如果已修改)
當時使用EF Core時,最後的 Update 更新操作並不是必須的,應為有 狀態變更跟蹤。但是建議顯式呼叫,適配其他資料庫提供程式。
示例中 IssueAssignDto
是一個簡單的 DTO 類:
using System;
namespace IssueTracking.Issues
{
public class IssueAssignDto
{
public Guid IssueId{get;set;}
public Guid UserId{get;set;}
}
}
學習幫助
圍繞DDD和ABP Framework兩個核心技術,後面還會陸續釋出核心構件實現、綜合案例實現系列文章,敬請關注!
ABP Framework 研習社(QQ群:726299208)
專注 ABP Framework 學習及DDD實施經驗分享;示例原始碼、電子書共享,歡迎加入!