系列文章
- 基於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實施經驗分享;示例源碼、電子書共享,歡迎加入!