基於ABP落地領域驅動設計-06.正確區分領域邏輯和應用邏輯
系列文章
- 基於ABP落地領域驅動設計-00.目錄和前言
- 基於ABP落地領域驅動設計-01.全景圖
- 基於ABP落地領域驅動設計-02.聚合和聚合根的最佳實踐和原則
- 基於ABP落地領域驅動設計-03.倉儲和規約最佳實踐和原則
- 基於ABP落地領域驅動設計-04.領域服務和應用服務的最佳實踐和原則
- 基於ABP落地領域驅動設計-05.實體建立和更新最佳實踐
- 基於ABP落地領域驅動設計-06.正確區分領域邏輯和應用邏輯
圍繞DDD和ABP Framework兩個核心技術,後面還會陸續釋出核心構件實現、綜合案例實現系列文章,敬請關注!
ABP Framework 研習社(QQ群:726299208)
ABP Framework 學習及實施DDD經驗分享;示例原始碼、電子書共享,歡迎加入!
領域邏輯和應用邏輯
正如前面提到的,領域驅動設計中的業務邏輯拆分為兩部分:領域邏輯和應用邏輯。
領域邏輯由系統的核心領域規則組成,而應用程式邏輯實現特定於應用程式的用例。
雖然定義很清楚,但實現可能並不容易,常常無法決定哪些程式碼應該放在應用層,哪些程式碼應該放在領域層。本節試圖解釋這些差異。
多應用層
當你的系統很大時,DDD有助於處理複雜性問題。特別是,在一個領域中開發多個應用,那麼領域邏輯與應用邏輯的分離就變得更加重要。
假設你正在構建一個有多個應用程式的系統:
- 一個Web應用程式,使用 ASP .NET Core MVC,展示產品給使用者。瀏覽產品,不需要進行身份驗證;只有當用戶執行某些操作時,比如向購物車中新增產品,才會要求登陸。
- 一個後臺管理應用程式,使用Angular UI+ REST APIs構建。此應用由公司辦公人員做系統管理,比如:編輯產品描述。
- 一個移動應用程式,和 Web應用程式 相比UI更加簡單,通過REST APIs或其他技術(如:TCP/Socket)與伺服器通訊。
每一個應用都需要解決不同的需求,實現不同用例(應用服務方法),不同DTO,不同驗證和授權規則等等。
將所有這些邏輯混合到一個應用層中,將使你的服務包含太多的判斷條件和複雜的業務邏輯,使程式碼更難開發、維護和測試,並導致潛在的Bug。
如果你有單個領域關聯多個應用程式:
- 為每個應用程式或客戶端建立單獨的應用層,在這些單獨層中實現特定於應用的業務邏輯。
- 使用單個領域層共享核心領域邏輯。
這樣的設計使得區分領域邏輯和應用邏輯變得更加重要。
為了更清楚地實現,可以為每種應用程式型別建立不同的專案(.csproj
)。
例如:
- 後臺管理應用建立
IssueTracker.Admin.Application
和IssueTracker.Admin.Application.Contracts
專案 - WEB應用建立
IssueTracker.Public.Application
和IssueTracker.Public.Application.Contracts
- 移動應用建立
IssueTracker.Mobile.Application
和IssueTracker.Mobile.Application.Contracts
示例:正確區分應用邏輯和領域邏輯
本節包含一些應用服務和領域服務示例,討論如何決定在這些服務中放置業務邏輯。
示例:在領域服務中建立 Organization (組織)
public class OrganizationManager:DomainService
{
private readonly IRepository<Organization> _organizationRepository;
private readonly ICurrentUser _currentUser;
private readonly IAuthorizationService _authorizationService;
private readonly IEmailSender _emailSender;
public OrganizationManager(
IRepository<Organization> organizationRepository,
ICurrentUser currentUser,
IAuthorizationService authorizationService,
IEmailSender emailSender
)
{
_organizationRepository=organizationRepository;
_currentUser=currentUser;
_authorizationService=authorizationService;
_emailSender=emailSender;
}
//建立組織
public async Task<Organization> CreateAsync(string name)
{
//檢測是否存在同名組織,存在則丟擲異常。
if(await _organizationRepository.AnyAsync(x=>x.Name==name))
{
throw new BusinessException("IssueTracking:DuplicateOrganizationName");
}
//檢測是否擁有建立許可權
await _authorizationService.CheckAsync("OrganizationCreationPermission");
//記錄日誌
Logger.LogDebug($"Creating organization {name} by {_currentUser.UserName}");
//建立組織例項
var organization = new Organization();
//傳送提醒郵件
await _emailSender.SendAsync(
"[email protected]",
"新組織",
"新組織名稱:"+name
);
//返回組織例項
return organization;
}
}
讓我們一步一步來分析 CreateAsync
方法中的程式碼是否都應該放在領域服務中:
- 正確:組織名重複檢測,存在重複名稱則丟擲異常。該檢測與核心領域規則相關,不允許重名。
- 錯誤:領域服務不應該進行許可權驗證。許可權驗證應該放在應用層。
- 錯誤:記錄日誌包含當前使用者的使用者名稱。領域服務不應該依賴當前使用者,當前使用者(Session)應該是展示層或應用層中的相關概念。
- 錯誤:建立新組織傳送郵件,我們仍然認為這是業務邏輯。可以能會根據用例來建立不同型別郵件。
示例:在應用層建立新組織
public class OrganizationAppService:ApplicationService
{
private readonly OrganizationManager _organizationManager;
private readonly IPaymentService _paymentService;
private readonly IEmailSender _emailSender;
public OrganizaitonAppService(
OrganizationManager organizationManager,
IPaymentService paymentService,
IEmailSender emailSender
)
{
_organizationManager=organizationManager;
_paymentService=paymentService;
_emailSender=emailSender;
}
[UnitOfWork]
[Authorize("OrganizationCreationPermission")]
public async Task<Organization> CreateAsync(CreateOrganizationDto input)
{
//支付組織費用
await _paymentService.ChargeAsync(
CurrentUser.Id,
GetOrganizationPrice()
);
//建立組織例項
var organization = await _organizationManager.CreateAsync(input.Name);
//儲存組織到資料庫
await _organizationManager.InsertAsync(organization);
//傳送提醒郵件
await _emailSender.SendAsync(
"[email protected]",
"新組織",
"新組織名稱:"+name
);
//返回例項
return organization;
}
private double GetOrganizationPrice()
{
return 42;//Gets form somewhere...
}
}
讓我們看看 CreateAsync 方法,一步一步討論其中的程式碼是否應該放在應用服務:
- 正確:應用服務方法應該是工作單元,ABP框架工作單元系統自動實現,可以不用新增
[UnitOfWork]
特性。 - 正確:許可權驗證應該放在應用層,可以使用
[Authorize]
特性。 - 正確:在我們的業務邏輯中建立組織是付費服務,當前操作呼叫基礎設施服務進行支付操作。
- 正確:應用服務方法負責儲存變更到資料庫。
- 正確:給系統管理員傳送郵件通知。
- 錯誤:不能返回實體,應該返回DTO。
討論:為什麼我們不應該將支付邏輯放在領域服務中?
你可能想知道為什麼付款程式碼不在 OrganizationManager
裡面。這是一件很重要的事情,我們絕不希望付款出錯。
然而,業務的重要性並不意味著要將其視為核心業務邏輯。我們可能有其他的支付用例,在這些用例中,我們不收取費用來建立一個新的組織。
例如:
- 後臺辦公系統中的管理員使用者可以建立新組織,不用考慮支付。
- 系統資料匯入、整合、同步,也可能需要在沒有任何支付操作的情況下,建立組織。
如您所見,支付不是建立一個有效組織的必要操作。它是一個特定於用例的應用邏輯。
示例:CRUD操作
public class IssueAppService
{
private readonly IssueManager _issueManager;
public IssueAppService(IssueManager issueManager)
{
_issueManager=issueManager;
}
public async Task<IssueDto> GetAsync(Guid id)
{
return await _issueManager.GetAsync(id);
}
public async Task CreateAsync(IssueCreationDto input)
{
await _issueManager.CreateAsync(input);
}
public async Task UpdateAsync(UpdateIssueDto input)
{
await _issueManager.UpdateAsync(input);
}
public async Task DeleteAsync(Guid id)
{
await _issueManager.DeleteAsync(id);
}
}
應用服務並沒有做任何事情,而是委託給領域服務來處理。只接收DTO引數,並傳遞給 IssueManger
。
- 不要建立只實現簡單 CRUD 操作的領域服務方法,而不帶任何領域邏輯。
- 不要傳遞 DTO 給領域服務,或領域服務方法返回 DTO。
應用服務可以直接使用倉儲,實現查詢、建立、更新或刪除資料,除非執行這些操作時需要處理領域邏輯,這種情況下,建立領域服務方法,但只針對那些真正需要的方法。
不要因為將來可能會需要這些CRUD領域服務方法,就去提前建立這些方法! 當需要時再去建立,並重構現有的程式碼。由於抽象了應用層,重構領域層不會影響到UI層和其他客戶端。
學習幫助
圍繞DDD和ABP Framework兩個核心技術,後面還會陸續釋出核心構件實現、綜合案例實現系列文章,敬請關注!
ABP Framework 研習社(QQ群:726299208)
專注 ABP Framework 學習及DDD實施經驗分享;示例原始碼、電子書共享,歡迎加入!