📐 微服務設計原則:DDD 與邊界劃分
📌 Domain-Driven Design (DDD) 基礎概念
DDD 是一種軟體設計方法,以業務領域為核心來組織程式碼。在微服務架構中,DDD 幫助我們找到正確的服務邊界。
為什麼 DDD 對微服務這麼重要? 因為微服務拆分的依據不是技術層(Controller、Service、Repository),而是業務邊界。
📌 核心概念:通用語言 (Ubiquitous Language)
開發團隊與業務專家使用同一套語言來描述系統:
// ❌ 技術導向的命名
public class DataProcessor
{
public void ProcessRecord(int recordId) { }
}
// ✅ 業務導向的命名(通用語言)
public class OrderService
{
public void PlaceOrder(PlaceOrderCommand command) { }
public void CancelOrder(Guid orderId, string reason) { }
public void ShipOrder(Guid orderId, ShippingInfo shipping) { }
}
📌 Bounded Context 限界上下文
限界上下文是 DDD 中最重要的概念之一。同一個名詞在不同的上下文中可能有不同的含義。
例子:"產品" 在不同上下文的意義
// ── 商品目錄上下文 (Catalog Context) ──
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal ListPrice { get; set; }
public List<string> Images { get; set; }
public Category Category { get; set; }
}
// ── 訂單上下文 (Order Context) ──
// 同樣叫 "Product",但只需要訂單相關的資訊
public class OrderItem
{
public Guid ProductId { get; set; }
public string ProductName { get; set; } // 快照,不是引用
public decimal UnitPrice { get; set; } // 下單時的價格
public int Quantity { get; set; }
}
// ── 庫存上下文 (Inventory Context) ──
public class StockItem
{
public Guid ProductId { get; set; }
public string Sku { get; set; }
public int QuantityOnHand { get; set; }
public int ReorderThreshold { get; set; }
public string WarehouseLocation { get; set; }
}
重點: 每個限界上下文有自己的 "Product" 模型,只包含該上下文需要的屬性。
📌 聚合根、實體與值物件
聚合根 (Aggregate Root)
聚合根是外部存取聚合的唯一入口,確保業務規則的一致性。
// Order 是聚合根
public class Order
{
public Guid Id { get; private set; }
public Guid CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
private readonly List<OrderLine> _lines = new();
public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
// 業務邏輯都透過聚合根來操作
public void AddItem(Guid productId, string productName,
decimal unitPrice, int quantity)
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("只有草稿狀態的訂單可以加入商品");
var existing = _lines.FirstOrDefault(l => l.ProductId == productId);
if (existing != null)
{
existing.IncreaseQuantity(quantity);
}
else
{
_lines.Add(new OrderLine(productId, productName, unitPrice, quantity));
}
}
public void Submit()
{
if (!_lines.Any())
throw new InvalidOperationException("訂單至少要有一個商品");
Status = OrderStatus.Submitted;
// 發出領域事件
AddDomainEvent(new OrderSubmittedEvent(Id, CustomerId, GetTotal()));
}
public decimal GetTotal() => _lines.Sum(l => l.SubTotal);
}
實體 (Entity) — 有唯一識別的物件
// OrderLine 是實體(有 Id),屬於 Order 聚合
public class OrderLine
{
public Guid Id { get; private set; }
public Guid ProductId { get; private set; }
public string ProductName { get; private set; }
public decimal UnitPrice { get; private set; }
public int Quantity { get; private set; }
public decimal SubTotal => UnitPrice * Quantity;
public OrderLine(Guid productId, string productName,
decimal unitPrice, int quantity)
{
Id = Guid.NewGuid();
ProductId = productId;
ProductName = productName;
UnitPrice = unitPrice;
Quantity = quantity;
}
public void IncreaseQuantity(int amount)
{
if (amount <= 0) throw new ArgumentException("數量必須大於 0");
Quantity += amount;
}
}
值物件 (Value Object) — 用值來比較的物件
// Address 是值物件:沒有 Id,用值來比較
public record Address(
string Street,
string City,
string State,
string ZipCode,
string Country)
{
// C# record 自動實作值比較
// new Address("信義路", "台北", ...) == new Address("信義路", "台北", ...)
}
// Money 也是值物件
public record Money(decimal Amount, string Currency)
{
public static Money operator +(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("不同幣別無法直接相加");
return new Money(a.Amount + b.Amount, a.Currency);
}
}
📌 如何劃分微服務的邊界
步驟 1:識別業務領域
電商系統的業務領域:
├── 用戶管理(註冊、登入、個人資料)
├── 商品目錄(瀏覽、搜尋、分類)
├── 訂單管理(下單、取消、查詢)
├── 庫存管理(庫存數量、進出貨)
├── 支付處理(付款、退款)
├── 物流配送(出貨、追蹤)
└── 通知服務(Email、SMS、推播)
步驟 2:畫出上下文映射圖 (Context Map)
┌──────────┐ 同步呼叫 ┌──────────┐
│ 訂單服務 │ ───────────→ │ 庫存服務 │
│ │ │ │
└────┬─────┘ └──────────┘
│ 發布事件
↓
┌──────────┐ ┌──────────┐
│ 支付服務 │ │ 通知服務 │
│ │ │ (訂閱事件)│
└──────────┘ └──────────┘
📌 資料庫分離策略:Database per Service
// 每個微服務有自己的 DbContext 和資料庫
// ── 訂單服務 ──
public class OrderDbContext : DbContext
{
public DbSet<Order> Orders { get; set; }
public DbSet<OrderLine> OrderLines { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseNpgsql("Host=order-db;Database=OrderDb");
}
// ── 庫存服務 ──
public class InventoryDbContext : DbContext
{
public DbSet<StockItem> StockItems { get; set; }
public DbSet<Warehouse> Warehouses { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseNpgsql("Host=inventory-db;Database=InventoryDb");
}
黃金法則: 服務之間絕不直接存取對方的資料庫,只透過 API 或事件溝通。
📌 範例:電商系統的服務拆分
| 服務 | 職責 | 資料庫 | 主要 API |
|---|---|---|---|
| UserService | 用戶註冊、登入、Profile | PostgreSQL | POST /api/users, GET /api/users/{id} |
| CatalogService | 商品 CRUD、搜尋 | PostgreSQL + Elasticsearch | GET /api/products, GET /api/products/{id} |
| OrderService | 下單、訂單狀態管理 | PostgreSQL | POST /api/orders, GET /api/orders/{id} |
| InventoryService | 庫存管理、扣減 | PostgreSQL | POST /api/inventory/reserve, PUT /api/inventory/release |
| PaymentService | 支付、退款 | PostgreSQL | POST /api/payments, POST /api/refunds |
| NotificationService | 發送通知 | MongoDB | 透過訊息佇列觸發 |
下一章: 我們將動手用 ASP.NET Core 建立第一個微服務 API。