1. 程式人生 > 其它 >基於ABP落地領域驅動設計-04.領域服務和應用服務的最佳實踐和原則

基於ABP落地領域驅動設計-04.領域服務和應用服務的最佳實踐和原則

目錄

系列文章

圍繞DDDABP 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;}
  }
}

學習幫助

圍繞DDDABP Framework兩個核心技術,後面還會陸續釋出核心構件實現綜合案例實現系列文章,敬請關注!

ABP Framework 研習社(QQ群:726299208)
專注 ABP Framework 學習及DDD實施經驗分享;示例原始碼、電子書共享,歡迎加入!