ドメイン駆動設計(DDD)入門|エンジニアのためのモデリング基礎

kento_morota 12分で読めます

「仕様通りに実装したはずなのに、ビジネス部門から『違う』と言われる」「コードが複雑になりすぎて、機能追加のたびに予想外のバグが発生する」——こうした課題の根本にあるのは、ソフトウェアの設計がビジネスの実態と乖離していることです。

ドメイン駆動設計(Domain-Driven Design、DDD)は、ビジネスドメインの知識をソフトウェア設計の中心に据えることで、この問題を解決するアプローチです。本記事では、DDDの核心的な概念と実践的なモデリング手法を、具体例を交えて解説します。

DDDとは何か?なぜ必要なのか

ドメイン駆動設計は、2003年にEric Evansが著書『Domain-Driven Design』で提唱した設計思想です。「ドメイン」とはソフトウェアが対象とするビジネス領域を指し、DDDはそのドメインの知識と構造をソフトウェア設計に直接反映させることを目指します。

従来の設計手法の限界

多くのプロジェクトでは、データベーステーブルの構造をそのままコードに反映するデータ中心の設計が行われています。この方式では、テーブルのCRUD操作を中心にコードが組み立てられ、ビジネスルールはServiceクラスに散らばりがちです。

プロジェクトの初期段階ではこの方式でも問題ありませんが、ビジネスが成長しルールが複雑になると、以下の問題が顕在化します。

ビジネスルールの散在
同じルールが複数のServiceクラスに重複して実装され、変更時に漏れが発生します。

ドメイン知識の喪失
コードからビジネスの意図を読み取ることが難しくなり、新しいメンバーの立ち上がりに時間がかかります。

変更コストの増大
ビジネスルールの変更が広範囲のコード修正を要求し、リリースのリスクが高まります。

DDDが解決するもの

DDDは、ビジネスの専門家(ドメインエキスパート)とエンジニアが共通の言語(ユビキタス言語)を使ってコミュニケーションし、その言語をそのままコードに落とし込むことで、設計とビジネスの乖離を防ぎます。

コードが「ビジネスの言葉」で書かれるため、ビジネスルールの変更がコードの変更に自然に対応し、保守性と拡張性が向上します。

ユビキタス言語(Ubiquitous Language)

ユビキタス言語は、DDDの最も基本的かつ重要な概念です。プロジェクトに関わる全員(ビジネス部門、エンジニア、デザイナー)が共通して使う用語の集合を指します。

ユビキタス言語が必要な理由

開発の現場では、同じ概念をビジネス部門とエンジニアが異なる言葉で表現することが頻繁にあります。ビジネス部門が「受注」と呼ぶものをエンジニアが「オーダーレコード」と呼んだり、「顧客」と「ユーザー」が混在したりすると、コミュニケーションコストが増大し、誤解が生まれます。

ユビキタス言語では、ビジネスの概念に対する名前を1つに統一し、会話でもドキュメントでもコードでも同じ用語を使います。

ユビキタス言語の作り方

ドメインエキスパートとの対話
ビジネス部門のメンバーと定期的にディスカッションし、業務で使われている用語とその定義を収集します。

用語集の作成
収集した用語をチーム全員がアクセスできる場所に一覧化します。各用語の定義、使用例、関連する用語を記載します。

コードへの反映
クラス名、メソッド名、変数名にユビキタス言語の用語をそのまま使用します。たとえば「注文を確定する」という業務操作は、order.confirm()としてコードに表現します。

継続的な改善
ユビキタス言語は固定ではなく、プロジェクトの進行とともに洗練されていきます。新しい概念が発見されたら用語集を更新し、コードにも反映します。

戦術的設計:DDDの構成要素

DDDには、ドメインモデルを構築するための具体的な構成要素(ビルディングブロック)が定義されています。主要なものを解説します。

エンティティ(Entity)

一意の識別子(ID)を持ち、ライフサイクルを通じて同一性を保つオブジェクトです。属性が変化しても、IDが同じであれば同一のエンティティとみなされます。

たとえば「ユーザー」はエンティティです。名前やメールアドレスが変わっても、ユーザーIDが同じなら同一のユーザーです。エンティティは自身に関するビジネスルールを内部に持ち、不正な状態への遷移を防ぎます。

値オブジェクト(Value Object)

識別子を持たず、属性の値そのものに意味があるオブジェクトです。同じ属性を持つ値オブジェクトは等価とみなされます。不変(イミュータブル)に設計するのが原則です。

「金額」は典型的な値オブジェクトです。「1,000円」という金額は、どのインスタンスであっても等しいです。値オブジェクトに通貨の種類と金額をセットで持たせることで、「異なる通貨の金額を加算してしまう」といったバグを型レベルで防げます。

メールアドレス、住所、日付範囲、色など、多くの概念を値オブジェクトとしてモデリングできます。プリミティブ型(Stringやintなど)を使う代わりに値オブジェクトを使うことで、ビジネスルールをオブジェクト内にカプセル化できます。

集約(Aggregate)

関連するエンティティと値オブジェクトのまとまりで、データの一貫性を保つ単位です。集約にはルートエンティティ(集約ルート)があり、外部からの操作はすべて集約ルートを通じて行われます。

ECサイトの「注文」を例にすると、注文(Order)が集約ルートで、注文明細(OrderLine)や配送先住所(ShippingAddress)が集約内の要素です。注文明細の追加や変更は、必ずOrderエンティティのメソッドを通じて行い、Orderが注文全体の整合性を保証します。

集約設計の原則

集約は小さく保つことが重要です。大きすぎる集約はパフォーマンスの問題や同時更新の競合を引き起こします。異なる集約間の参照は、オブジェクトの直接参照ではなくIDで行います。

ドメインサービス(Domain Service)

特定のエンティティや値オブジェクトに属さないドメインロジックを実装する場所です。複数の集約にまたがる操作や、外部サービスとの連携が必要なビジネスルールをドメインサービスに配置します。

たとえば「送金」処理は、送金元口座と送金先口座の2つの集約にまたがるため、TransferServiceというドメインサービスとして実装するのが適切です。

リポジトリ(Repository)

集約の永続化と取得を担当するインターフェースです。ドメイン層はリポジトリのインターフェースのみに依存し、実際のデータアクセスの詳細(SQL、ORM、外部APIなど)はインフラ層で実装します。

これにより、ドメインロジックがデータアクセス技術に依存しなくなり、テストの容易性と将来の技術変更への柔軟性が確保されます。

戦略的設計:境界づけられたコンテキスト

DDDの戦略的設計は、大規模なシステムをどのように分割・統合するかを扱います。その中心にあるのが「境界づけられたコンテキスト(Bounded Context)」です。

境界づけられたコンテキストとは

同じ言葉が、ビジネスの文脈によって異なる意味を持つことがあります。たとえば「商品」は、商品カタログのコンテキストでは名前・説明・画像を持つ情報ですが、在庫管理のコンテキストでは数量・倉庫の場所を持つ情報であり、配送のコンテキストではサイズ・重量を持つ情報です。

これらを1つの「商品」モデルで表現しようとすると、あらゆる文脈の属性が詰め込まれた巨大で複雑なモデルになります。境界づけられたコンテキストは、この問題を解決するために、モデルが有効な範囲を明確に区切る概念です。

コンテキストマップ

複数の境界づけられたコンテキスト間の関係を示す図がコンテキストマップです。コンテキスト間の統合パターンとして、以下のようなものがあります。

共有カーネル(Shared Kernel)
2つのコンテキストが共通のモデルを共有します。密結合になるため、変更時は双方の合意が必要です。

カスタマー/サプライヤー(Customer/Supplier)
上流のコンテキスト(サプライヤー)が下流(カスタマー)の要件を考慮してAPIを提供します。

腐敗防止層(Anti-Corruption Layer)
外部システムやレガシーシステムとの間に変換層を設け、外部モデルの影響が自コンテキストに侵入するのを防ぎます。

公開ホストサービス(Open Host Service)
明確なプロトコル(REST APIなど)を公開し、他のコンテキストが利用できるようにします。

実践:ECサイトのドメインモデリング

具体例として、ECサイトのドメインモデルを設計する流れを見てみましょう。

ドメインの分析

まず、ドメインエキスパートとの対話を通じて、ビジネスプロセスと用語を洗い出します。

ECサイトのビジネスでは「顧客が商品を閲覧し、カートに追加し、注文を確定し、決済を行い、商品が配送される」という一連の流れがあります。この中から、主要な概念を抽出します。

境界づけられたコンテキストの特定

分析の結果、以下のコンテキストを特定します。

商品カタログコンテキスト
商品情報、カテゴリ、レビューの管理を担当します。

注文コンテキスト
カート、注文、注文明細の管理を担当します。

決済コンテキスト
支払い方法、決済処理、返金処理を担当します。

配送コンテキスト
配送先、配送状況、配送業者との連携を担当します。

顧客管理コンテキスト
会員情報、認証、ポイント管理を担当します。

注文コンテキストのモデリング例

注文コンテキストの集約を設計します。注文(Order)を集約ルートとし、以下の構成にします。

Order(集約ルート・エンティティ)は、注文ID、顧客ID(他コンテキストへの参照)、注文ステータス、注文明細のリスト、配送先住所(値オブジェクト)を持ちます。

OrderLine(エンティティ)は、明細ID、商品ID(他コンテキストへの参照)、数量、単価(値オブジェクトのMoney型)を持ちます。

OrderエンティティにaddItem()removeItem()confirm()cancel()といったメソッドを定義し、注文に関するビジネスルール(最低注文金額、注文可能ステータスの制御など)をOrder内部にカプセル化します。

DDDの導入でよくある失敗と対策

DDDの導入には落とし穴があります。よくある失敗パターンと対策を紹介します。

過剰なモデリング

すべての機能をDDDの原則に従って設計しようとすると、シンプルなCRUD処理にまで不必要な複雑さが生じます。DDDが効果を発揮するのは複雑なビジネスロジックがある箇所です。単純なCRUDにはシンプルな設計を適用し、DDDは本当に必要な部分に集中しましょう。

技術先行の設計

DDDのパターン(エンティティ、値オブジェクト、集約など)の「形」を先に決めてからビジネスロジックを当てはめるのは、DDDの本質に反します。まずビジネスの理解を深め、その結果としてパターンが自然に現れるようにしましょう。

ドメインエキスパート不在の設計

エンジニアだけでドメインモデルを設計すると、技術的には正しくてもビジネスの実態と乖離したモデルになります。ドメインエキスパートとの継続的なコミュニケーションがDDD成功の最大の鍵です。

境界づけられたコンテキストの粒度

コンテキストを細かく分割しすぎると、コンテキスト間の統合コストが膨大になります。逆に大きすぎると、モデルが肥大化して管理困難になります。最初は大きめに取り、理解が深まるにつれて必要に応じて分割していくのが実践的です。

DDDを始めるためのステップ

DDDは一度にすべてを導入する必要はありません。段階的に取り入れましょう。

ステップ1:ユビキタス言語の確立

最も手軽に始められるのが、チーム内の用語統一です。ビジネス部門と共通の用語集を作成し、コードにもその用語を反映させましょう。これだけでもコミュニケーションの質が大幅に改善します。

ステップ2:値オブジェクトの導入

既存コードのプリミティブ型を値オブジェクトに置き換えることから始めましょう。メールアドレス、金額、電話番号などをStringやintから専用の型に変更するだけで、バリデーションロジックの集約と型安全性の向上が実現します。

ステップ3:集約の設計

新しい機能を開発する際に、集約パターンを適用してみましょう。ビジネスルールをエンティティ内にカプセル化し、不変条件を保護する設計を実践します。

ステップ4:境界づけられたコンテキストの適用

システムが大規模になり、複数チームで開発する段階になったら、境界づけられたコンテキストを明確にし、コンテキスト間の統合パターンを設計します。

まとめ

ドメイン駆動設計は、ビジネスの複雑さにソフトウェアの設計で立ち向かうためのアプローチです。ユビキタス言語による共通理解、エンティティ・値オブジェクト・集約による戦術的設計、境界づけられたコンテキストによる戦略的設計が三本柱です。

DDDの導入は段階的に行い、まずは用語の統一と値オブジェクトの導入から始めましょう。複雑なビジネスドメインを扱うプロジェクトでは、DDDの考え方がコードの品質と保守性を長期的に向上させてくれます。重要なのはパターンの適用ではなく、ビジネスの本質を理解し、それをコードに正しく反映することです。

#DDD#ドメイン駆動設計#モデリング
共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

AI活用のヒントをお探しですか?お気軽にご相談ください。

まずは話だけ聞いてもらう