🕌

DDDと責務、Architecture..②モデリング手法(集約とコンテキスト)

2024/06/15に公開

DDDのモデリング手法

前回は、DDDの概要についての記事を書きましたが、今回は少し踏み込み...

今回のテーマは、DDDにおけるモデリングの中核である
「集約」と「境界づけられたコンテキスト(バウンデッドコンテキスト)」です。
この2つの要素を理解することで、
システム全体のアーキテクチャが柔軟かつ保守しやすくなるという重要なポイントです。

なぜこれが重要なのか?先に少し要約すると、
集約はデータの一貫性を保証し、
境界づけられたコンテキスト(バウンデッドコンテキスト)はシステムを適切な範囲に分割することで、
複雑なシステムでも効率的に管理
できるようにするためです。
これらの概念は、実際の開発現場で直面する課題を解決するための強力なツールとなります。

DDD要約
ドメイン駆動設計 (Domain-Driven Design: DDD) は、
複雑なソフトウェアシステムの開発において、
ビジネスドメインの知識を中心に据えて設計・開発を行う手法だ。
[DDDの目的]
ビジネスの専門家と開発者が共通の言語(ユビキタス言語)を使ってコミュニケーションを取りながら、ビジネスロジックを反映したコードを作成すること.

本からの引用も含めながらかいていきます。
違うのでは?などあったらコメントで教えてください:)

集約 (Aggregate)

ビジネスドメインの一貫性を保つためのモデルのことで、
一貫性を保証する単位でもあるDDDの基本的な構成要素の一つだ。
"必ず守りたい強い整合性を持ったオブジェクトのまとまり"を表す。

"集約"は、一つまたは複数のEntityや値オブジェクト(VO)で構成され、
その中の一つのEntityが「集約ルート (Aggregate Root)」として機能する。

[集約の特徴]

  • 一貫性境界 (Consistency Boundary):
    集約は、一貫性を保証する単位のこと。
    集約内のすべての変更は、トランザクションの範囲内で一貫して行われる。

  • 集約ルート:
    集約ルートは、集約のエントリーポイントであり、外部からのアクセスは
    このルートを通じて行われる。(集約ルート以外のエンティティは、外部から直接操作されない。)

  • 独立性:
    集約は他の集約と独立しており、相互に影響を与えない。
    これにより、システム全体の複雑さを低減し、変更の影響範囲を限定する。

これを踏まえて...具体的にしていこう。

設計/実装時のルール

  • 強い整合性確保が必要なものを1つの集約にする
  • トランザクションを一つにする: 集約内のオブジェクトの整合性を保つため

■ 強い整合性確保が必要なものを1つの集約に

Ex. 注文集約
以下のコードは、注文管理システムにおける注文集約の例だ。
注文のモデルを考えてみる。

注文は複数の注文アイテムを含み、注文の状態は一貫性を持って管理される。

この時に強い整合性が必要なのは、
"注文の状態 (status) と注文アイテム (OrderItem)"
の一貫性だ。

具体的には、注文が確定される前に少なくとも一つの注文アイテムが含まれていることや、
出荷後には注文をキャンセルできないことなどのビジネスルールを守る必要がある。

Order が集約ルートであり、OrderItem がその内部のエンティティ。
Order は単一のトランザクション内で操作され、
注文の状態(status)は一貫性を持って管理される。

コード
CartId
data class CartId(val id: String)
Order
class Order(
    val id: OrderId,
    private val items: MutableList<OrderItem> = mutableListOf(),
    var status: OrderStatus = OrderStatus.CREATED
) {
    fun addItem(item: OrderItem) {
        items.add(item)
    }

    fun removeItem(productId: String) {
        items.removeIf { it.productId == productId }
    }

    fun confirmOrder() {
        if (items.isEmpty()) {
            throw IllegalStateException("Order must contain at least one item")
        }
        status = OrderStatus.CONFIRMED
    }

    fun cancelOrder() {
        if (status == OrderStatus.SHIPPED) {
            throw IllegalStateException("Cannot cancel an order that has been shipped")
        }
        status = OrderStatus.CANCELLED
    }
}
OrderItem
data class OrderItem(
    val productId: String,
    val price: Double,
    val quantity: Int
)
OrderStatus
enum class OrderStatus {
    CREATED, CONFIRMED, SHIPPED, CANCELLED
}

■ トランザクションを一つにする

単一のトランザクション内で行われることにより、
集約内のオブジェクトの整合性が確保され、システムの整合性が保たれる。

どういうことかというと、

<単一トランザクション>

  • 注文が確定されるときに、
    → 注文の状態が「確認済み」に変更され、注文アイテムが適切に更新される必要がある。
    = これらの変更は、単一のトランザクション内で行われるため、一貫性が保たれるし、
     一部の変更が失敗した場合は、全体がロールバックされ、
     システムの一貫性が失われることを防具ことができる。

境界つけられたコンテキスト (Bounded Context)

ビジネスドメインを適切な境界で区切り、
各コンテキスト内で一貫したモデルを構築するための概念だ。

コンテキスト: システムの特定の部分に関する用語やルールが適用される範囲

DDD refarenceでは以下のように記載されている

A description of a boundary (typically a subsystem, or the work of a particular team) within which a particular model is defined and applicable.
特定のモデルが定義される境界(ex.サブシステム、特定のチームの作業)を記述したもの。

Evans本では以下のように記載されている

Explicitly define the context within which a model applies. Explicitly set boundaries in terms of team organization, usage within specific parts of the application, and physical manifestations such as code bases and database schemas. Keep the model strictly consistent within these bounds, but don't be distracted or confused by issues outside.

モデルが適用されるコンテキストを明確に定義する。チームの組織、アプリケーション内の特定部分での使用、コードベースやデータベーススキーマといった物理的な現れなど、境界を明確に設定する。その範囲内でモデルを厳密に一貫させるが、その外部の問題に気を取られたり混乱したりしないようにする。

これは結論で、正直私はこれだとよくわからないので砕く。

そもそものDDDの目的に立ち戻って考えると...

大規模な(複雑な)システム開発において、ビジネスドメインに焦点を当てて
ビジネスの核心部分(ドメイン)を明確にし、その理解をモデル化することで
システムがビジネスのニーズを正確に対応、反映し変化に強い設計を行おう!というものだ。

システムが大規模になればなるほど、関係者すべてで統一したモデルを作ることは難しくなる。

例えば、eコマースシステムでは以下のようなバウンデッドコンテキストが考えられる。

  • 商品管理コンテキスト: 商品の情報や在庫管理
  • 注文管理コンテキスト: 顧客の注文や支払い処理
  • 配送管理コンテキスト: 配送の手配や追跡

これを一つのコンテキストにしてしまって、これで一つと境界づけてしまったら...
みんな扱うのは同じで"商品"だとしても、

  • 商品管理、で言ったら服そのものの商品をイメージする
  • 注文管理、で言ったら注文書の商品
  • 配送管理、で言ったら梱包済みでの商品の形になる

すべての関係者で統一したモデルを作ることは現実的ではないし、
莫大な規模のモデルになてしまい、トランザクション範囲は広くなるし、いいことがない!

chatGPTに絵を描いてもらってみたw

この問題について、Evans本では次のように語られている。

"In those younger days we were advised to build a unified model of the entire business, but DDD recognizes that we’ve learned that 'total unification of the domain model for a large system will not be feasible or cost-effective'"

昔はビジネス全体の統一モデルを構築するようにアドバイスされていましたが、
DDDは「大規模システムのドメインモデルの完全な統合は実現不可能で、費用対効果が悪い」
と認識しています。

そこで、DDDではモデルが適用される範囲を明示的に定義し、それぞれの中で
モデルとユビキタス言語の統一を目指す。
この明示的に定義された範囲を「境界づけられたコンテキスト(バウンデッドコンテキスト)」と呼んでいる、というわけだ!

これらを踏まえた実装...

に関しては、
アーキテクチャの理解を入れながらやるので次回とします!

ここまで読んでくれてありがとうございました!

Discussion