第九章:領域驅動設計 (DDD) 理論與實踐
9.1 引言:為什麼需要 DDD?
在複雜的企業軟體開發中,最大的挑戰往往不是技術,而是業務邏輯的複雜性。領域驅動設計 (DDD) 提供了一套方法論,幫助我們將複雜的業務需求轉化為清晰的軟體模型。
ABP Framework 是一個 Opinionated (有主見的) 框架,它將 DDD 的最佳實踐直接內建在架構中。本章將帶您理解這些核心概念。
9.2 核心概念解析
1. 實體 (Entity)
實體是具有 唯一識別碼 (Identity) 的物件。即使兩個實體的屬性完全相同,只要 ID 不同,它們就是不同的物件。
- 例子:
User(使用者),Order(訂單)。 - ABP 實作:繼承
Entity<TKey>。
2. 值物件 (Value Object)
值物件沒有唯一識別碼,它是透過 屬性值 來定義的。如果兩個值物件的所有屬性都相同,它們就被視為相等。值物件通常是 不可變的 (Immutable)。
- 例子:
Address(地址 - 包含城市、街道),Money(金額 - 包含數值、幣別)。 - ABP 實作:繼承
ValueObject。
3. 聚合 (Aggregate) 與 聚合根 (Aggregate Root)
聚合是一組相關物件的集合,它們被視為一個修改的單元。
- 聚合根:是聚合中唯一允許外部直接引用的實體。外部物件只能透過聚合根來存取聚合內部的其他實體。
- 原則:
- 交易邊界:一個交易通常只修改一個聚合。
- 級聯刪除:刪除聚合根時,聚合內的所有物件也應被刪除。
- ABP 實作:繼承
AggregateRoot<TKey>。
4. 領域服務 (Domain Service)
當某個業務邏輯不屬於任何單一實體或值物件時,我們將其放入領域服務中。
- 例子:轉帳 (涉及兩個帳戶實體)。
- ABP 實作:繼承
DomainService。
9.3 聚合設計原則
設計良好的聚合是 DDD 成功的關鍵。
1. 定義值物件 (Address)
csharp
public class Address : ValueObject
{
public string City { get; private set; }
public string Street { get; private set; }
private Address() { } // ORM 需要
public Address(string city, string street)
{
City = city;
Street = street;
}
protected override IEnumerable<object> GetAtomicValues()
{
yield return City;
yield return Street;
}
}2. 定義聚合根 (Order)
csharp
public class Order : FullAuditedAggregateRoot<Guid>
{
public Guid CustomerId { get; private set; } // 引用其他聚合
public Address ShippingAddress { get; private set; } // 值物件
public List<OrderItem> Items { get; private set; } // 聚合內部實體
public decimal TotalPrice { get; private set; }
private Order() { }
public Order(Guid id, Guid customerId, Address address) : base(id)
{
CustomerId = customerId;
ShippingAddress = address;
Items = new List<OrderItem>();
}
// 業務方法:新增項目
public void AddItem(Guid productId, decimal price, int quantity)
{
if (quantity <= 0) throw new BusinessException("Order:InvalidQuantity");
var existingItem = Items.FirstOrDefault(i => i.ProductId == productId);
if (existingItem != null)
{
existingItem.IncreaseQuantity(quantity);
}
else
{
Items.Add(new OrderItem(Id, productId, price, quantity));
}
RecalculateTotal();
}
private void RecalculateTotal()
{
TotalPrice = Items.Sum(x => x.Price * x.Quantity);
}
}3. 定義聚合內部實體 (OrderItem)
csharp
public class OrderItem : Entity<Guid>
{
public Guid OrderId { get; private set; }
public Guid ProductId { get; private set; }
public decimal Price { get; private set; }
public int Quantity { get; private set; }
internal OrderItem(Guid orderId, Guid productId, decimal price, int quantity)
: base(Guid.NewGuid())
{
OrderId = orderId;
ProductId = productId;
Price = price;
Quantity = quantity;
}
internal void IncreaseQuantity(int quantity)
{
Quantity += quantity;
}
}注意 internal 建構子與方法,這強迫外部只能透過 Order 聚合根來操作 OrderItem。
9.5 領域事件 (Domain Events)
當領域中發生了重要的事情時,我們發布領域事件。
1. 定義事件
csharp
public class OrderCreatedEvent
{
public Guid OrderId { get; set; }
public Guid CustomerId { get; set; }
}2. 發布事件 (在聚合根中)
csharp
public class Order : AggregateRoot<Guid>
{
public void Complete()
{
// ... 狀態變更邏輯
AddDomainEvent(new OrderCreatedEvent { OrderId = Id, CustomerId = CustomerId });
}
}ABP 會在 SaveChangesAsync 被呼叫時自動發送這些事件。
3. 訂閱事件 (Handler)
csharp
public class OrderCreatedEventHandler : IDistributedEventHandler<OrderCreatedEvent>, ITransientDependency
{
public async Task HandleEventAsync(OrderCreatedEvent eventData)
{
// 發送 Email 通知客戶
await _emailSender.SendAsync(eventData.CustomerId, "訂單成立通知", "...");
}
---
## 9.7 總結
本章介紹了 DDD 的核心戰術模式。
- **聚合** 是資料一致性的守門員。
- **值物件** 讓模型更具表達力。
- **領域事件** 實現了系統的解耦。
在下一章,我們將深入探討 **領域服務 (Domain Services)** 與 **規約模式 (Specification Pattern)**,進一步完善我們的領域層實作。
---
**參考資源**:
- [ABP 領域驅動設計文件](https://docs.abp.io/en/abp/latest/Domain-Driven-Design)