TL;DR

  • イミュータブルなデータでは、子のエンティティを 1 つ変えるだけでも、トップレベルの集約ルートのレベルで作業する必要がある。
  • 集約は整合性の境界であり、不変条件が適用される場所であり、永続化の基本単位でもある。

内容

集約と集約ルート

Order(注文)と OrderLine(注文明細)を例に考える。Order は詳細が時間とともに変わっても同じ注文なので、エンティティである。OrderLine も、数量や価格が変わっても同じ明細なので、エンティティである。

では、ある明細を変更したら、それが属する注文も変更したことになるのか。イミュータブルなデータ構造では、そうなる。イミュータブルな Order がイミュータブルな OrderLine のコレクションを持つ場合、Order 全体を作り直す必要がある。

let changeOrderLinePrice order orderLineId newPrice =
    // 1. orderLineId で変更する明細を探す
    let orderLine = order.OrderLines |> findOrderLine orderLineId
    // 2. 新しい価格を持つ明細を作る
    let newOrderLine = { orderLine with Price = newPrice }
    // 3. 古い明細を置き換えた、新しい明細のリストを作る
    let newOrderLines =
        order.OrderLines |> replaceOrderLine orderLineId newOrderLine
    // 4. 明細のリストを置き換えた、新しい注文を作る
    let newOrder = { order with OrderLines = newOrderLines }
    // 5. 注文を返す
    newOrder

このように、子のエンティティを 1 つ変えるだけでも、常に Order のレベルで作業する。これはよくあることなので名前が付いている。エンティティのまとまりを集約、Order のように集約のトップにあるエンティティを集約ルートと呼ぶ。

集約による整合性と不変条件の担保

集約は、データが更新されるときの整合性の境界として働く。集約のある部分が更新されたら、整合性を保つためにほかの部分も更新する必要がある。たとえば、Order に合計金額を保存しているなら、明細の価格が変わったときに合計も更新する。この処理は上の changeOrderLinePrice の中で行う。

集約は不変条件が適用される場所でもある。たとえば「Order には少なくとも 1 つの OrderLine がある」という条件である(第6章)。

集約の参照

ドメインオブジェクトに、ほかの集約のレコードそのものを持たせてはいけない。次のように Customer を持たせると、イミュータブルであるがゆえに、Customer を更新するたびに Order も更新しなければならなくなる。

type Order = {
    OrderId : OrderId
    Customer : Customer
    OrderLines : OrderLine list
}

代わりに、参照(アイデンティティ)を持たせる。

type Order = {
    OrderId : OrderId
    CustomerId : CustomerId
    OrderLines : OrderLine list
}

永続化の単位

集約は永続化の基本単位である。データベースからロード・セーブするときは、集約全体を扱う。シリアライズしてネットワークで送るときも同じである。データベースのトランザクションは 1 つの集約だけを扱い、複数の集約を含めたり、集約の境界を越えたりしてはならない。

集約は、単なるエンティティの集まりではない。たとえば Customers(Customer のリスト)はエンティティの集まりだが、トップレベルのエンティティをルートに持たず、整合性の境界にもならないので、集約ではない。

キーワード