引子:為什麼我們需要 DDD?
想象你接手了一個電商系統的重構項目。打開代碼一看:訂單、支付、庫存、物流的邏輯散落在各個 Service 裏,一個 OrderService 有 3000 行代碼,方法名叫 processOrder、handleOrder、dealOrder,看起來都差不多但又不知道具體幹啥。更可怕的是,業務規則藏在各個角落:有的在 Controller 做校驗,有的在 Service 計算,有的在數據庫觸發器裏...
這就是典型的"貧血模型 + 事務腳本"帶來的問題。而 DDD(Domain-Driven Design,領域驅動設計)就是為了解決這類問題而生的。
一、DDD 的核心思想:以領域為中心
1.1 什麼是領域(Domain)?
領域就是你要解決的業務問題空間。 比如電商領域、金融領域、物流領域。
DDD 的第一個核心觀點:軟件的複雜度來自業務本身,而不是技術。 所以我們要把精力放在理解業務上,而不是一上來就想着用什麼框架、什麼中間件。
舉個例子:
- ❌ 傳統思維:"這個功能需要一個訂單表、一個訂單詳情表、然後寫個 CRUD..."
- ✅ DDD 思維:"在我們的業務裏,'訂單'代表什麼?它有哪些狀態?什麼情況下可以取消?取消後庫存怎麼處理?"
1.2 通用語言(Ubiquitous Language)
DDD 強調團隊要建立通用語言:開發、產品、運營都用同一套術語。
比如在電商領域:
- "訂單"不叫 Order,叫 PurchaseOrder(採購訂單)
- "支付"不叫 pay(),叫 completePayment()(完成支付動作)
- "取消"不叫 delete(),叫 cancel()(業務動作)
代碼就是文檔,變量名就是業務語言。 當產品經理説"訂單履約",你的代碼裏就應該有 OrderFulfillment 這個類。
二、戰略設計:劃分領域邊界
2.1 限界上下文(Bounded Context)
這是 DDD 最重要的概念之一。不同的上下文中,同一個詞可能有不同的含義。
以"商品"為例:
- 商品上下文:商品有 SKU、SPU、規格、價格
- 庫存上下文:商品只關心庫存數量、倉庫位置
- 營銷上下文:商品關心優惠活動、促銷標籤
所以我們要劃分限界上下文,每個上下文有自己的模型:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 商品上下文 │ │ 庫存上下文 │ │ 訂單上下文 │
│ │ │ │ │ │
│ Product │───▶│ Stock │◀───│ Order │
│ - skuId │ │ - skuId │ │ - orderId │
│ - name │ │ - quantity │ │ - items[] │
│ - price │ │ - warehouse │ │ - totalAmount │
└─────────────────┘ └─────────────────┘ └─────────────────┘
2.2 上下文映射(Context Mapping)
不同上下文之間如何交互?DDD 定義了幾種模式:
1) 共享內核(Shared Kernel)
- 兩個上下文共享一部分模型
- 適合緊密協作的團隊
2) 客户-供應商(Customer-Supplier)
- 下游依賴上游的接口
- 比如訂單上下文依賴庫存上下文的接口查庫存
3) 防腐層(Anti-Corruption Layer, ACL)
- 這是最常用的!當你對接外部系統或遺留系統時,用 ACL 做一層轉換
舉個例子,對接第三方支付:
// 防腐層:隔離外部系統
public class PaymentAdapter {
private ThirdPartyPaymentClient client;
public PaymentResult pay(Order order) {
// 將我們的領域模型轉換為第三方格式
ThirdPartyRequest request = convertToThirdParty(order);
ThirdPartyResponse response = client.pay(request);
// 將第三方結果轉換為我們的領域模型
return convertToDomain(response);
}
}
防腐層的價值:當第三方系統變更或更換時,只需要修改 ACL,核心領域模型不受影響。
2.3 子域劃分
把整個業務領域劃分為:
- 核心域(Core Domain):你的競爭力所在,比如電商的定價策略、推薦算法
- 支撐域(Supporting Domain):必須有但不是核心競爭力,比如用户管理
- 通用域(Generic Domain):可以買現成方案的,比如短信發送、文件存儲
資源分配原則:核心域投入最好的人力,支撐域夠用就行,通用域儘量用開源/SaaS。
三、戰術設計:構建領域模型
3.1 實體(Entity)vs 值對象(Value Object)
實體:有唯一標識,生命週期中屬性可變
public class Order {
private OrderId id; // 唯一標識
private Money totalAmount;
private OrderStatus status;
// 業務方法
public void cancel() {
if (status == OrderStatus.PAID) {
throw new OrderCannotCancelException("已支付訂單不能取消");
}
this.status = OrderStatus.CANCELLED;
}
}
值對象:沒有標識,通過屬性判斷相等,不可變
public class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金額不能為負");
}
this.amount = amount;
this.currency = currency;
}
public Money add(Money other) {
// 返回新對象,不修改自己
return new Money(this.amount.add(other.amount), this.currency);
}
}
為什麼要區分?
- 實體關注"是誰"(身份),值對象關注"是什麼"(屬性)
- 值對象可以共享、緩存,實體不行
- 兩個金額 100 元是相等的(值對象),但兩個訂單 ID 相同只能説明是同一個訂單(實體)
3.2 聚合(Aggregate)與聚合根
聚合是一組相關對象的集合,有一個聚合根作為入口。
比如訂單聚合:
Order (聚合根)
├── OrderItem (訂單明細)
├── ShippingAddress (收貨地址)
└── Payment (支付信息)
聚合的設計原則:
1) 邊界內強一致性,邊界外最終一致性
- 訂單和訂單明細必須同時保存(強一致性)
- 訂單和庫存可以異步更新(最終一致性)
2) 通過聚合根修改
// ❌ 錯誤:直接修改訂單明細
orderItem.setQuantity(10);
// ✅ 正確:通過聚合根
order.updateItemQuantity(itemId, 10);
3) 聚合根持有其他實體的引用,外部只持有聚合根的 ID
public class Order {
private OrderId id;
private CustomerId customerId; // 只持有 ID,不持有整個 Customer 對象
private List<OrderItem> items; // 持有內部實體
}
聚合大小的經驗法則:
- 聚合不要太大(控制在 2-3 層),否則併發衝突多、性能差
- 如果兩個對象總是一起修改,放在一個聚合;否則拆分
3.3 領域服務(Domain Service)
當一個業務邏輯不屬於某個實體時,用領域服務。
比如"轉賬"操作涉及兩個賬户:
public class TransferService {
public void transfer(Account from, Account to, Money amount) {
from.debit(amount); // 扣款
to.credit(amount); // 入賬
// 記錄轉賬事件
domainEventPublisher.publish(new MoneyTransferred(from.id(), to.id(), amount));
}
}
領域服務 vs 應用服務:
- 領域服務:包含業務邏輯,比如"轉賬規則"
- 應用服務:編排流程,比如"調用轉賬、發通知、記日誌"
3.4 倉儲(Repository)
倉儲是領域對象的持久化抽象,隱藏數據庫細節。
public interface OrderRepository {
Order findById(OrderId id);
void save(Order order);
List<Order> findByCustomerId(CustomerId customerId);
}
關鍵點:
- 接口定義在領域層,實現在基礎設施
- 用領域語言(findByCustomerId),而不是 SQL 語言(selectByCustomerId)
- 返回的是聚合根,而不是貧血的 PO 對象
3.5 領域事件(Domain Event)
當領域中發生重要的事情時,發佈事件。
public class Order {
public void pay() {
this.status = OrderStatus.PAID;
// 發佈領域事件
DomainEventPublisher.publish(new OrderPaidEvent(this.id, this.totalAmount));
}
}
// 事件處理器
@EventHandler
public class OrderPaidEventHandler {
public void handle(OrderPaidEvent event) {
// 扣減庫存、發送通知等
inventoryService.reserve(event.getOrderId());
notificationService.sendPaymentConfirmation(event.getOrderId());
}
}
領域事件的價值:
- 解耦:訂單不需要知道支付後要做什麼
- 可擴展:新增需求時只需要加事件處理器
- 最終一致性:跨聚合的操作通過事件異步完成
四、分層架構:讓領域模型不受污染
DDD 典型的四層架構:
┌─────────────────────────────────────┐
│ User Interface Layer (接口層) │ ← Controller、DTO
├─────────────────────────────────────┤
│ Application Layer (應用層) │ ← ApplicationService、事務
├─────────────────────────────────────┤
│ Domain Layer (領域層) │ ← Entity、ValueObject、DomainService
├─────────────────────────────────────┤
│ Infrastructure Layer (基礎設施層) │ ← Repository 實現、MQ、DB
└─────────────────────────────────────┘
依賴規則:只能向下依賴,領域層不依賴任何外層!
舉個例子:
// 應用層:編排流程
public class OrderApplicationService {
private OrderRepository orderRepository;
private InventoryService inventoryService;
@Transactional
public void placeOrder(PlaceOrderCommand command) {
// 1. 構建領域對象
Order order = Order.create(command.getCustomerId(), command.getItems());
// 2. 執行領域邏輯
order.place(); // 領域方法
// 3. 檢查庫存(領域服務)
inventoryService.checkAndReserve(order.getItems());
// 4. 持久化
orderRepository.save(order);
}
}
五、實戰:從貧血模型到充血模型
貧血模型(反模式)
// 數據對象
public class Order {
private Long id;
private Integer status;
private BigDecimal totalAmount;
// 只有 getter/setter
}
// Service 裏寫所有邏輯
public class OrderService {
public void cancelOrder(Long orderId) {
Order order = orderDAO.selectById(orderId);
if (order.getStatus() == 2) {
throw new Exception("已支付不能取消");
}
order.setStatus(3);
orderDAO.update(order);
}
}
問題:
- 業務邏輯散落在各個 Service
- Order 對象沒有行為,只是數據容器
- 無法保證業務規則一致性(到處都能 setStatus)
充血模型(DDD)
// 領域對象:有數據也有行為
public class Order {
private OrderId id;
private OrderStatus status;
private Money totalAmount;
public void cancel() {
if (status == OrderStatus.PAID) {
throw new OrderCannotCancelException("已支付訂單不能取消");
}
this.status = OrderStatus.CANCELLED;
// 發佈領域事件
DomainEventPublisher.publish(new OrderCancelledEvent(this.id));
}
// 不提供 setStatus,只能通過業務方法修改狀態
}
優勢:
- 業務規則封裝在領域對象內
- 狀態變更有跡可循(通過方法名就知道發生了什麼)
- 測試簡單(不需要數據庫,直接測試領域對象)
六、DDD 的常見誤區
誤區 1:DDD = 微服務
- DDD 是設計方法,微服務是架構風格
- 可以在單體應用中用 DDD,也可以在微服務中不用 DDD
- 但 DDD 的限界上下文確實為微服務拆分提供了指導
誤區 2:DDD 必須用充血模型
- 充血模型是 DDD 的常見實現方式,但不是唯一方式
- 簡單的 CRUD 場景用貧血模型也沒問題
- 只有複雜業務邏輯才需要 DDD
誤區 3:為了 DDD 而 DDD
- 不要過度設計,小項目用 DDD 是殺雞用牛刀
- DDD 的價值在於"駕馭複雜度",簡單系統沒必要
誤區 4:DDD 就是設計很多類
- 實體、值對象、聚合根...是工具,不是目的
- 關鍵是理解業務、建立統一語言、劃分清晰邊界
七、DDD 在 Java 生態中的實踐
7.1 框架選擇
- Spring Boot:最常見,用 @Service、@Repository 分層
- Axon Framework:專為 CQRS + Event Sourcing 設計
- JMolecules:提供 DDD 註解(@Entity、@ValueObject)
7.2 持久化
// 領域層:接口
public interface OrderRepository {
Order findById(OrderId id);
void save(Order order);
}
// 基礎設施層:JPA 實現
@Repository
public class OrderJpaRepository implements OrderRepository {
@PersistenceContext
private EntityManager em;
public Order findById(OrderId id) {
OrderPO po = em.find(OrderPO.class, id.getValue());
return OrderMapper.toDomain(po); // PO → 領域對象
}
public void save(Order order) {
OrderPO po = OrderMapper.toPO(order); // 領域對象 → PO
em.merge(po);
}
}
7.3 事件驅動
// 使用 Spring Event
@Service
public class OrderDomainService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void payOrder(Order order) {
order.pay();
eventPublisher.publishEvent(new OrderPaidEvent(order.getId()));
}
}
// 事件監聽
@Component
public class InventoryEventListener {
@EventListener
@Async
public void handle(OrderPaidEvent event) {
// 扣減庫存
}
}