領域驅動設計(DDD)完整教學文件
從戰略設計到戰術設計,事件風暴與實際休假系統範例的深入解析
精要亮點
- 全面覆蓋戰略與戰術設計:從劃分限界上下文、識別業務子領域,到具體的實體、值對象與領域服務設計。
- 事件風暴整合應用:詳細描述如何從提取事件、定義命令、確定參與者和外部系統,並通過完整的事件流鏈接整個流程。
- 實際開發範例:以企業使用的休假系統為例,提供可執行的C#程式碼、完整的程式碼結構圖,以及與CRUD資料設計的優缺點比較。
1. 領域驅動設計(DDD)概述
領域驅動設計(Domain-Driven Design, DDD)是一種強調以業務領域知識為核心的軟體設計方法。它主張軟體結構應直接映射到業務概念,並且與領域專家緊密溝通,共同建構領域模型。DDD主要分為兩大階段:戰略設計和戰術設計。戰略設計側重於理解、劃分業務領域(子領域)及限界上下文;而戰術設計則關注如何利用實體、值對象、聚合、領域服務、倉儲等模式將戰略設計落實到具體的程式碼中。
2. 戰略設計
戰略設計主要目的是從整體業務角度理解及拆解一個複雜系統,劃分子領域,並明確各個限界上下文之間的邊界與關係。這一階段不僅關注業務本身,而且強調開發團隊與領域專家間建立通用語言,以便在後續設計與溝通中能保持一致。
2.1 識別與切分業務領域
在企業休假系統中,需要首先識別系統中涉及的所有子領域。可以依據業務重要性與功能劃分其中的核心子領域、支持子領域以及通用子領域:
- 核心子領域:請假申請與審核流程,因為該流程直接影響公司的員工管理和出勤狀態。
- 支持子領域:例如特休餘額檢查、系統通知等,這些都是輔助核心流程的重要支撐。
- 通用子領域:如資料管理、用戶認證等,可在多個系統中進行重用。
2.2 限界上下文(Bounded Context)
限界上下文是指在特定的業務領域中,使用通用業務語言構造的概念範圍。對於休假系統而言,可以定義多個上下文,如「請假管理上下文」、「特休檢查上下文」及「報公系統上下文」。每一個上下文內部擁有獨立、清晰的領域模型,而上下文之間則通過上下文映射進行整合。
2.3 通用語言的重要性
建立一套通用語言使團隊成員在討論業務概念時不會產生歧義。例如,在休假系統中,所有參與者都認同「請假申請」、「特休餘額」、「請假審核」等定義與流程。這將有助於戰略設計的順利推進。
2.4 事件風暴在戰略設計中的應用
事件風暴是一種以貼紙記錄事件、命令及參與者的協作工作坊方法。透過事件風暴可以快速識別關鍵業務事件,並將這些事件連結成一個完整的業務流程。以修行家系統中的休假申請流程為例,事件風暴的步驟包括:
步驟一:識別業務事件
將企業中所有關鍵業務行為以貼紙方式記錄下來,常見的事件包括:
- 請假申請提交
- 特休餘額檢查
- 請假申請審核(批准或拒絕)
- 休假記錄建立
- 更新報公系統
步驟二:識別命令與角色
命令代表觸發事件的動作,例如:SubmitLeaveApplication、ApproveLeaveApplication和RejectLeaveApplication。角色則有員工、主管及系統,這些參與者共同構成了業務流程。
步驟三:識別外部系統
在部分流程中,可能涉及外部系統。例如,休假申請通過後需同步更新到公司的報公系統。這一點在事件風暴過程中需要特別標明其數據交互和整合方式。
3. 戰術設計
戰術設計是將戰略設計成果落實到實際程式碼中的過程。這一階段圍繞領域模型進行具體的架構拆分與模式設計,並以C#為例進行實作。
3.1 領域模型與主要模式
在休假系統中,根據業務需求可定義以下模式:
- 實體(Entities):如LeaveApplication(請假申請)和Employee(員工),具有獨立身份和狀態。
- 值對象(Value Objects):如DateRange(請假日期範圍),沒有身份識別但封裝業務屬性。
- 聚合(Aggregates):領域內相關實體和值對象的集合,如LeaveApplication聚合。
- 領域服務(Domain Services):封裝跨多個實體的業務邏輯,如請假審核服務。
- 倉儲(Repositories):負責持久化領域對象,提供數據存取接口。
- 工廠(Factories):用於創建復雜聚合的實例。
3.2 事件風暴在戰術設計中的延伸
戰術設計階段依然需要運用事件風暴中的成果,將事件、命令以及流程細化到程式層面。例如,當員工提交請假申請後,系統將自動檢查特休餘額、進行請假審核,並在最終更新請假狀態後觸發對應的領域事件。這些領域事件不僅促進聚合間的解耦,同時也能作為系統通信機制,在不同上下文間傳遞信息。
4. 修行家休假系統案例詳細說明
以下以企業常用的休假系統作為實際案例說明,詳細描述如何應用事件風暴從初步提取事件到最終落實程式碼的完整流程。
4.1 案例業務流程
案例中包含以下業務流程:
- 請假申請提交:員工通過系統提交請假申請。
- 特休餘額檢查:系統自動檢查員工剩餘特休天數,確保申請合理。
- 請假審核:主管對請假申請進行審核,進而批准或拒絕該申請。
- 請假記錄建立:當申請經過審核,系統便會建立正式的請假記錄。
- 同步更新報公系統:將審核通過的申請推送至報公系統,以便統一管理行政流程。
4.2 事件風暴過程應用詳解
利用事件風暴方法,步驟如下:
(1) 事件提取
將所有業務中關鍵的行為用貼紙記錄,範例如下:
- LeaveApplicationSubmitted(請假申請提交)
- LeaveBalanceChecked(特休餘額檢查)
- LeaveApplicationApproved/Rejected(請假審核通過或拒絕)
- LeaveRecordCreated(請假記錄建立)
- PublicReportingSystemUpdated(更新報公系統)
(2) 命令與參與者識別
對應提取的事件,識別啟動流程的命令和參與者:
- 命令:SubmitLeaveApplication、ApproveLeaveApplication、RejectLeaveApplication
- 參與者:Employee(員工)、Manager(主管)、System(系統)
(3) 外部系統與溝通
在流程中,明確標記與外部系統的接口,例如:報公系統負責處理審核後的統計與記錄。
5. C# 程式碼及結構圖說明
以下提供一份依據上述業務流程設計的C#程式碼,完整涵蓋請假申請、特休檢查、審核流程及同步更新外部系統的邏輯。這份程式碼可直接執行,並附有程式碼結構圖以便理解各模組之間的配合方式。
5.1 C# 程式碼範例
// using directives
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
// 定義領域事件接口
public interface IDomainEvent
{
DateTime OccurredOn { get; }
}
// 領域事件:請假申請提交事件
public class LeaveApplicationSubmitted : IDomainEvent
{
public DateTime OccurredOn { get; } = DateTime.UtcNow;
public Guid ApplicationId { get; set; }
public Guid EmployeeId { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
// 實體:請假申請
public class LeaveApplication
{
public Guid Id { get; private set; }
public Guid EmployeeId { get; private set; }
public DateTime StartDate { get; private set; }
public DateTime EndDate { get; private set; }
public string Status { get; private set; } // Pending, Approved, Rejected
public LeaveApplication(Guid employeeId, DateTime startDate, DateTime endDate)
{
Id = Guid.NewGuid();
EmployeeId = employeeId;
StartDate = startDate;
EndDate = endDate;
Status = "Pending";
}
public void Approve()
{
Status = "Approved";
}
public void Reject()
{
Status = "Rejected";
}
}
// 值對象:請假日期範圍
public class DateRange
{
public DateTime StartDate { get; }
public DateTime EndDate { get; }
public DateRange(DateTime start, DateTime end)
{
StartDate = start;
EndDate = end;
}
public int GetTotalDays() => (EndDate - StartDate).Days + 1;
}
// 值對象:員工休假餘額
public class LeaveBalance
{
public int AvailableDays { get; private set; }
public LeaveBalance(int days)
{
AvailableDays = days;
}
public bool CheckIfEnough(int requestedDays)
{
return AvailableDays >= requestedDays;
}
public void DeductDays(int days)
{
if (CheckIfEnough(days))
{
AvailableDays -= days;
}
else
{
throw new Exception("Insufficient leave balance.");
}
}
}
// 實體:員工
public class Employee
{
public Guid Id { get; private set; }
public string Name { get; private set; }
public LeaveBalance Balance { get; private set; }
public Employee(string name, int leaveDays)
{
Id = Guid.NewGuid();
Name = name;
Balance = new LeaveBalance(leaveDays);
}
}
// 領域服務:處理請假申請服務
public class LeaveApplicationService
{
private readonly ILeaveApplicationRepository _repository;
private readonly IEventBus _eventBus;
public LeaveApplicationService(ILeaveApplicationRepository repository, IEventBus eventBus)
{
_repository = repository;
_eventBus = eventBus;
}
public async Task SubmitLeaveApplication(Guid employeeId, DateTime startDate, DateTime endDate)
{
// 實時檢查邏輯(假定外部服務檢查特休餘額)
int requestedDays = (endDate - startDate).Days + 1;
// 這裡可加入查詢該員工的邏輯
// 假設檢查通過後
var application = new LeaveApplication(employeeId, startDate, endDate);
await _repository.Save(application);
var domainEvent = new LeaveApplicationSubmitted
{
ApplicationId = application.Id,
EmployeeId = employeeId,
StartDate = startDate,
EndDate = endDate
};
await _eventBus.Publish(domainEvent);
}
public async Task ApproveLeaveApplication(Guid applicationId)
{
var application = await _repository.GetById(applicationId);
application.Approve();
await _repository.Update(application);
// 推送領域事件,這裡可通知報公系統更新資料
}
}
// 領域事件總線接口
public interface IEventBus
{
Task Publish<T>(T @event) where T : IDomainEvent;
}
// 倉儲:請假申請持久化接口
public interface ILeaveApplicationRepository
{
Task Save(LeaveApplication application);
Task<LeaveApplication> GetById(Guid id);
Task Update(LeaveApplication application);
}
// 簡單的事件總線實現(模擬異步操作)
public class SimpleEventBus : IEventBus
{
public Task Publish<T>(T @event) where T : IDomainEvent
{
Console.WriteLine($"Domain Event Published: {@event.GetType().Name} at {@event.OccurredOn}");
return Task.CompletedTask;
}
}
5.2 程式碼結構圖
| 模組 | 主要功能 |
|---|---|
| 領域層 |
|
| 應用服務層 |
|
| 基礎設施層 |
|
6. DDD 與 CRUD 資料設計比較
在設計複雜業務系統時,領域驅動設計與傳統的CRUD(Create, Read, Update, Delete)兩種方法各有優缺點。下表總結了兩者的主要差異:
| 比較項目 | 領域驅動設計(DDD) | CRUD |
|---|---|---|
| 業務邏輯 | 業務邏輯集中,模型貼近真實領域,使得系統更具彈性 | 業務邏輯常分散於資料存取層,難以應對複雜需求 |
| 模型演化 | 易於擴展與修改,適合變化頻繁的業務 | 修改可能牽一髮而動全身,維護成本高 |
| 學習曲線 | 概念與設計模式眾多,學習曲線較陡 | 實現簡單、上手快 |
| 適用場景 | 企業級、業務規則復雜且需要長期演進的系統 | 資料操作簡單、原型或小型應用 |
因此,在業務邏輯複雜、變化頻繁、需要與多個業務專家持續溝通的場景下,選用領域驅動設計更能反映真實業務,提高系統維護和擴展的能力;而在較簡單、數據為中心的應用中,CRUD則足以應付需求。
7. 結論與總結
本文件詳細闡述了領域驅動設計從戰略設計到戰術設計的完整過程,並結合事件風暴方法講解如何從提取事件、定義命令、確定參與者與外部系統接口,到最終落實至具體的C#程式碼實現企業休假系統。透過對比DDD與CRUD資料設計的優缺點,我們可以看出,DDD更適用於需要高度靈活性與業務複雜度處理的企業級應用,而CRUD方式則在簡單應用中有其快速實現的優勢。這份文件不僅提供了理論上的詳細介紹,還附帶了實際可執行的程式碼範例及系統結構圖,希望能為您的實際應用提供明確的參考與指導。
總結來說,領域驅動設計通過劃分限界上下文、建立清晰的業務模型並應用事件風暴來促進團隊協作與系統擴展,從而解決複雜業務需求下的軟體設計挑戰。希望這份文件能夠幫助公司內部團隊更全面地理解並運用DDD,提高軟體開發與維護效率。
參考資料
- 領域驅動設計 - 維基百科
- 領域驅動設計的奧秘:如何將複雜問題簡化為優雅模型?
- 領域驅動設計入門與概要 - IT人
- DDD的战略设计和战术设计 - Thoughtworks
- 【Day 07】領域驅動設計的戰術設計(Tactical Design) - iT 邦幫忙
相關推薦查詢
Last updated February 24, 2025