DDD | 04-什麼是聚合根

Neking發表於2024-07-15

三、什麼是聚合根?

聚合根(Aggregate Root)是DDD中的一個核心概念,用於組織和管理一組相關的領域物件,確保它們的整體一致性和完整性。聚合根是領域模型中的關鍵元件,它不僅封裝了領域內的複雜業務邏輯,還提供了控制訪問和維護資料一致性的機制,是構建可維護、可擴充套件的軟體系統的重要基石。

主要特點

  • 聚合的概念:聚合是一組相關物件的集合,這些物件一起形成一個完整的業務概念,並作為一個單元進行操作。在聚合內部,物件之間有嚴格的關聯和依賴關係。
  • 根實體:聚合根是聚合中的一個特殊實體,它是外部世界訪問聚合內部其他成員的唯一入口點。這意味著外部物件不能直接持有聚合內非根實體的引用,必須透過根實體來訪問和操作它們。
  • 邊界定義:每個聚合都有一個清晰的邊界,這個邊界定義了聚合的範圍,以及哪些操作可以在聚合內部執行,哪些操作需要透過根實體來協調。這有助於維護資料的一致性和完整性。
  • 一致性保證:聚合根負責維護其內部的所有業務規則和一致性約束。當外部嘗試修改聚合狀態時,所有必要的驗證和業務邏輯都在聚合根內部執行,確保操作的原子性和一致性。
  • 唯一標識:聚合根擁有一個全域性唯一的識別符號(ID),外部系統透過這個ID 來識別和訪問聚合。這個ID也是與其他聚合建立關聯的基礎。
  • 生命週期管理:聚合根還負責管理器內部物件的生命週期,包括建立、更新和刪除聚合內的實體和值物件。

設計原則

明確的邊界

  • 聚合根定義了一個清晰的邊界,限定那些物件屬於聚合內部,那些屬於外部。邊界內的物件作為一個整體處理,對外部隱藏內部細節

單一入口點

  • 聚合根是外界訪問聚合內部其他物件的唯一合法途徑。外部物件不能直接持有或操作聚合內的非根實體,所有互動都需透過根實體的方法進行

內聚性

  • 聚合內的所有物件應緊密相關,共同完成一個業務功能。這意味著聚合內的物件和操作應該圍繞著一個核心業務概念組織

一致性規則封裝

  • 聚合根負責維護其內部的一致性,包括業務規則、驗證邏輯等。所有改變聚合狀態的操作都應該在根實體中實現,確保操作的原子性和業務規則的遵守

標識唯一性

  • 每個聚合根都有一個全域性唯一的識別符號(ID), 用以區分不同的聚合例項。這個ID 也用於外部對聚合的引用

生命週期管理

  • 聚合根控制其內部成員(實體和值物件)的生命週期,包括它們的建立、更新和刪除

有限的大小

  • 為了保持聚合的可管理和易於理解,通常建議聚合的大小不要過大。過大的聚合可能導致效能問題和複雜度增加

聚合內部引用

  • 聚合內部的物件可以通常引用相互協作,但這些引用應限制在聚合邊界之內。對合聚合間的關聯,通常適用ID進行間接引用

事務邊界

  • 在事務處理中,聚合根常常作為事務的邊界,確保事務內的所有操作要麼全部成功,要麼全部失敗,以此來維護資料的完整性

問題探討

聚合根和實體物件有什麼區別?

身份標識(Identity)

  • 實體(Entity):實體具有唯一標識(ID),這個ID用來區分領域中的每一個單獨的實體例項。實體的ID在整個系統範圍內具有唯一性
  • 聚合根 (Aggregate Root):聚合根也是一個實體,但它在一個聚合中扮演領導角色。它的ID不僅在系統範圍內是唯一的,而且是外部世界訪問聚合內部其他實體的入口點

聚合邊界(Aggregate Boundary)

  • 實體:實體可以是聚合內部的一部分,也可以是獨立存在的。如果實體是聚合內部的一部分,則其ID在聚合內部唯一即可,外部訪問該實體必須透過聚合根
  • 聚合根:定義了聚合的邊界,決定了那些物件屬於聚合內部。外部物件不能直接訪問聚合內的非根實體,只能透過聚合根暴露的介面進行操作

職責與控制

  • 實體:實體主要負責維護自身的狀態和行為,可能包含一些簡單的業務邏輯
  • 聚合根:負責維護整個聚合的一致性,包括內部實體的狀態更改和業務規則的執行。它控制著對聚合內部元素的所有修改,確保在任何時刻聚合都處於有效狀態

生命週期關聯

  • 實體:實體的生命週期通常由其所在聚合根或外部服務(如Repository)管理
  • 聚合根:除了管理自己的生命週期外,還間接管理其內部實體的生命週期,決定它們的建立、更新和刪除

訪問控制

  • 實體:如果不是聚合根,實體通常不直接暴露給外部,外部元件不應直接持有實體的引用
  • 聚合根:是外部訪問聚合內部的唯一合法通道,提供對外介面,隱藏內部實現細節和複雜性

綜上所述,聚合根是一種特殊的實體,它不僅代表一個獨立的業務概念,還負責協調和保護其內部的其他實體和值物件,確保整體的業務規則得到執行,維持資料的一致性和完整性。實體則是領域模型中的基本構建塊,標識具有唯一標識的領域物件,而聚合根在實體之上提供了一層額外的結構和控制。

你覺得User是一個實體物件還是聚合根?

在大多數情況下,User 通常被設計為一個聚合根,因為它能更好地適應業務需求的變化和複雜性。

  • 唯一識別符號:User擁有一個全域性唯一的識別符號(如使用者ID),這滿足實體的基本特徵
  • 業務操縱的中心:使用者賬戶是許多業務操作的中心,如登陸、資料編輯、許可權管理等。這些操作往往涉及到使用者資訊的修改和驗證,因此需要一個統一的入口點來維護這些操作的一致性和安全性,這正是聚合根的作用
  • 關聯管理:使用者可能與其他領域物件關聯,比如使用者可能擁有多個地址、多個角色或者與多個訂單相關聯。作為聚合根,User 可以管理這些關聯關係,控制對這些關聯物件的訪問和修改,確保資料的完整性和一致性
  • 許可權和安全:在很多系統中,使用者資料是非常敏感的,需要嚴格控制訪問許可權。透過User 設計為聚合根,可以更集中地實施安全策略和訪問控制邏輯

程式碼示例

設計一個訂單聚合類(OrderAggregate)


/**
* 訂單聚合類,封裝了訂單的相關資訊和行為。
* 包括訂單ID、顧客ID、訂單狀態、訂單項、支付狀態等。
*/
public class OrderAggregate {
    private final UUID orderId;
    private final UUID customerId;
    private OrderStatusVO status;
    private final List<OrderLineItemEntity> lineItems;
    private boolean isPaid;

    /**
     * 建構函式初始化訂單聚合體。
     * @param orderId 訂單ID
     * @param customerId 顧客ID
     * @param status 訂單初始狀態
     */
    public OrderAggregate(UUID orderId, UUID customerId, OrderStatusVO status) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.status = status;
        this.lineItems = new ArrayList<>();
        this.isPaid = false;
    }

    /**
     * 新增訂單項。
     * 如果訂單已支付,則丟擲IllegalStateException異常。
     * @param lineItem 要新增的訂單項
     */
    public void addLineItem(OrderLineItemEntity lineItem) {
        if (!isPaid) {
            lineItems.add(lineItem);
        } else {
            throw new IllegalStateException("Cannot add line item to paid order");
        }
    }

    /**
     * 移除訂單項。
     * @param lineItem 要移除的訂單項
     */
    public void removeLineItem(OrderLineItemEntity lineItem) {
        lineItems.remove(lineItem);
    }

    /**
     * 標記訂單為已支付。
     * 如果訂單已支付,則丟擲IllegalStateException異常。
     */
    public void pay() {
        if (!isPaid) {
            isPaid = true;
            status = OrderStatusVO.PAID;
        } else {
            throw new IllegalStateException("Order is already paid");
        }
    }

    /**
     * 更改訂單狀態。
     * @param newStatus 新的訂單狀態
     */
    public void changeStatus(OrderStatusVO newStatus) {
        this.status = newStatus;
    }

    /**
     * 計算訂單的總金額。
     * @return 訂單的總金額
     */
    public BigDecimal calculateTotalAmount() {
        return lineItems.stream()
                .map(OrderLineItemEntity::calculateTotalPrice)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    /**
     * 獲取訂單ID。
     * @return 訂單ID
     */
    public UUID getOrderId() {
        return orderId;
    }

    /**
     * 獲取顧客ID。
     * @return 顧客ID
     */
    public UUID getCustomerId() {
        return customerId;
    }

    /**
     * 獲取訂單狀態。
     * @return 訂單狀態
     */
    public OrderStatusVO getStatus() {
        return status;
    }

    /**
     * 獲取訂單項列表。
     * @return 訂單項列表
     */
    public List<OrderLineItemEntity> getLineItems() {
        return lineItems;
    }

    /**
     * 檢查訂單是否已支付。
     * @return 如果訂單已支付返回true,否則返回false
     */
    public boolean isPaid() {
        return isPaid;
    }

    /**
     * 返回訂單聚合體的字串表示。
     * @return 訂單聚合體的字串表示
     */
    @Override
    public String toString() {
        return "OrderAggregate{" +
                "orderId=" + orderId +
                ", customerId=" + customerId +
                ", status=" + status +
                ", lineItems=" + lineItems +
                ", isPaid=" + isPaid +
                '}';
    }

    /**
     * 比較兩個訂單聚合體是否相等。
     * @param o 要比較的訂單聚合體
     * @return 如果兩個訂單聚合體相等返回true,否則返回false
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }

        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        OrderAggregate that = (OrderAggregate) o;

        return isPaid == that.isPaid && Objects.equals(orderId, that.orderId) && Objects.equals(customerId, that.customerId) && status == that.status && Objects.equals(lineItems, that.lineItems);
    }

    /**
     * 計算訂單聚合體的雜湊碼。
     * @return 訂單聚合體的雜湊碼
     */
    @Override
    public int hashCode() {
        return Objects.hash(orderId, customerId, status, lineItems, isPaid);
    }
}

相關文章