🫧
集約(Aggregate)
一貫性境界を持つエンティティ群のクラスター。集約ルートを通じてのみアクセスされる
解決する課題
複数のエンティティが関連し合うとき、どこかで変更が起きたとき全体の一貫性を誰が保証するのかが曖昧になる。Order と OrderItem、Cart と CartItem を別々に変更できてしまうと、不整合な状態が生まれる。
概念
集約(Aggregate) は、一貫性を保つ単位としてまとめたエンティティ・値オブジェクトのクラスター。
- 集約ルート(Aggregate Root): 集約全体への唯一のアクセス窓口となるエンティティ
- 外部から集約内部のオブジェクトへの直接参照は禁止
- 1トランザクション = 1集約の変更が原則
┌─────────────────────────────────┐
│ Order(集約ルート) │
│ ┌───────────┐ ┌──────────────┐ │
│ │ OrderItem │ │ ShippingInfo │ │
│ └───────────┘ └──────────────┘ │
└─────────────────────────────────┘
外部からは Order のメソッドを通じてのみ変更できる
集約ルートの責務
class Order: # 集約ルート
def add_item(self, product_id: ProductId, qty: int) -> None:
# 不変条件のチェック
if self._status != OrderStatus.DRAFT:
raise DomainException("確定済みの注文に商品を追加できません")
if qty <= 0:
raise DomainException("数量は1以上必要です")
self._items.append(OrderItem(product_id, qty))
def place(self) -> None:
if not self._items:
raise DomainException("商品なしで注文を確定できません")
self._status = OrderStatus.PLACED
self._events.append(OrderPlaced(str(self._id)))
集約間の参照はIDのみ
# ❌ 別集約への直接参照
class Order:
def __init__(self, customer: Customer):
self._customer = customer # Customer 集約を直接保持
# ✅ IDのみで参照
class Order:
def __init__(self, customer_id: CustomerId):
self._customer_id = customer_id # IDのみ
別集約への直接参照を許すと、変更の影響範囲が広がり一貫性境界が崩れる。
Vernon が挙げる設計原則(詳細は別記事)
- 真に必要な一貫性境界だけを集約に含める
- 集約は小さく保つ
- 結果整合性で解決できる部分は集約をまたいでよい
- 集約はIDで参照する
トランザクション境界
1トランザクションで複数の集約を変更してはいけない(結果整合性を使う)。
「注文確定 → 在庫減算」は同一トランザクションで行うのではなく、OrderPlaced イベントを受け取った在庫コンテキストが非同期で処理する。
関連概念
- 1. 🗺️サブドメイン(コア・サポート・汎用)
- 2. 🗾コンテキストマップ
- 3. ⬡六角形アーキテクチャ(ポートとアダプター)
- 4. 🪪エンティティ(Entity)
- 5. ⚙️ドメインサービス(Domain Service)
- 6. 📣ドメインイベント(Domain Event)
- 7. 🫧集約(Aggregate)
- 8. 📐集約の設計原則(Vernon の4ルール)
- 9. 🗄️リポジトリ(Repository)
- 10. 🏭ファクトリ(Factory)
- 11. 📦モジュール(Module)
- 12. 🎛️アプリケーションサービス(Application Service)
- 13. 🔌境界づけられたコンテキストの統合
- 14. ⚡CQRS(コマンドクエリ責任分離)
- 15. 📜イベントソーシング(Event Sourcing)
出典: 実践ドメイン駆動設計(Vaughn Vernon)第10章