初學者傾向將「購買商品」歸屬於 Product(商品)或 User(用戶)類,導致實體類膨脹、職責不清。
// 實體/值物件 - 僅狀態與自衛約束
public class Product {
public string Id { get; private set; }
public string Name { get; private set; }
public decimal Price { get; private set; }
public int Stock { get; private set; }
public void ReduceStock(int qty) { if (qty > Stock) throw new DomainException(\"庫存不足\"); Stock -= qty; }
}
public class User {
public string Id { get; private set; }
public string Name { get; private set; }
public UserLevel Level { get; private set; }
}
// 領域服務 - 協調購買流程
public class PurchaseService {
private readonly IProductRepository _productRepo;
private readonly IOrderRepository _orderRepo;
private readonly IPaymentGateway _payment;
private readonly ICouponService _coupon;
public Order Purchase(User buyer, string productId, int quantity, string couponCode) {
// 1. 獲取聚合根
Product product = _productRepo.Get(productId);
// 2. 業務驗證
if (product.Stock < quantity) throw new DomainException(\"庫存不足\");
// 3. 計算價格(含優惠)
decimal total = product.Price * quantity;
Coupon coupon = _coupon.Validate(couponCode, buyer, total);
total = coupon.Apply(total);
// 4. 支付
PaymentResult pay = _payment.Charge(buyer.Id, total);
if (!pay.Success) throw new DomainException(\"支付失敗: \" + pay.Message);
// 5. 扣減庫存、建立訂單
product.ReduceStock(quantity);
Order order = new Order(buyer.Id, productId, quantity, total, pay.TransactionId);
_orderRepo.Save(order);
// 6. 發送領域事件(可選)
DomainEvents.Raise(new OrderPlacedEvent(order));
return order;
}
}
| 放入 Product | 放入 User | 放入 PurchaseService |
|---|---|---|
| 違反單一職責:商品不應知道支付、優惠券、訂單 | 用戶不應知道庫存扣減、訂單持久化 | ✅ 職責單一:協調流程,不持有狀態 |
| 循環依賴:Product 需引用 Payment、Order、Coupon | User 需引用 Product、Order、Payment | ✅ 依賴抽象,易測試、易替換 |
| 難以單元測試(需 Mock 大量外部服務) | 同左 | ✅ 可 Mock Repository/Gateway 純單元測試 |
**購買商品屬於領域服務**,而非實體方法。實體專注「是什麼」(狀態+不變約束),服務專注「怎麼做」(跨實體流程協調)。這是 DDD 核心分層思想,也是從貧血模型走向豐富領域模型的關鍵一步。