引子:為什麼我們需要 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) {
        // 扣減庫存
    }
}