1. 概要
DDDでの開発未経験状態で、「3. 参考」にある文献をインプットして、自分の理解と過去の開発現場(DDD採用現場でない)における経験から、アレンジを加えつつ、自分の言葉でアウトプットして見直せるようにしたものです。
なので、正確性については責任取れません。あしからず。
2. DDD
2.1. コンセプト
- プログラム適用対象領域の概念・知識・制約・課題を把握
-
- をコードに落とし込む
といった「プログラミングによる課題解決フローを実践するための開発手法」。
その登場人物や工程を図にすると、以下のようなイメージになる。
上記イメージ図で登場する用語については、次の項で説明する。
2.2. 用語集
ドメイン
アプリケーション化対象となるシステムの領域。
ドメインエキスパート
「ドメイン」に最も詳しい人。
(普通は「プロダクトオーナー」「ディレクター」「プランナー」と呼ばれる人物だろう)
ドメインエキスパートは「ドメインについて最初からすべてを見通せている人物」だと思ってはいけない。
都度、最終成果物となるアプリケーション(のモック)などを見せて、ドメインに対する解像度を上げてもらうことを考える必要がある。
(といったようにDDDはアジャイル的な開発を前提にしているため、ウォーターフォール開発との相性が悪いといえる)
ドメインモデル
ドメインのコード化のために必要な要素を抽出して図示したもの。
以下2つの図から構成する(のが一般的?)
ユースケース図:
「誰」が「何」をするか を簡易的に表した図。
例. 一般的なシステムにおけるアカウント関連機能のユースケース図
ドメインモデル図:
ユースケースを満たすために必要なデータ構造を図示化したもの。
一般的なクラス図と違って、メソッドは書かない。
ユースケース図に含めなかった各ユースケースを実行するにあたってのドメイン知識(詳細条件等)は補足的に記述。
例. 上記ユースケースについてのドメインモデル図
ドメインモデルの作成は、「ドメインエキスパート」と「開発者」共同で行う(機能毎にドメインモデルを作るケースが多そうな印象である)。
ドメインモデルは、「ユビキタス言語」で構成されている必要がある。
ユビキタス言語
ドメインモデルを作成する上でのプロジェクトメンバー間の「共通言語」。
(当然だが、定義をサボると表記ゆれが発生し、プロジェクトをすすめる上でのノイズになってしまう & メンバー間での解釈ズレが生じやすくなるため、プロジェクトが円滑に進みづらくなってしまう)
ドメインオブジェクト
ドメインモデルを忠実に表現したコード。
以下、ドメインオブジェクトに関する用語説明になる。
値オブジェクト
「ドメインモデル図上のクラスの属性」 に相当するオブジェクト。
「ドメインモデル図上の属性」は、コード上ではプリティブ型として扱えるデータであるが、
以下の「属性」「メソッド」を持つクラスを実装し、このクラスのオブジェクトとしてを扱う。
●属性
「ドメインモデル図上のクラスの属性」をパーツごとに分割してそれぞれ「コード上のクラスの属性」として管理
例.
ドメインモデル図上のクラスの属性 | コード上のクラスの属性 |
---|---|
氏名 | - 氏 - 名 |
バージョン番号 | - メジャーバージョン番号 - マイナーバージョン番号 |
●メソッド
- コンストラクタ:
- 引数: 「ドメインモデル図上のクラスの属性」をパーツごとに分割したもの
- 内部処理:
- ①引数で受け取ったパーツ分割された 「ドメインモデル図上のクラスの属性」のバリデーションチェック(値チェック)
- ②値オブジェクトクラスの属性群に、引数で受け取った値をセット
- 値オブジェクト同士の四則演算、論理演算
- 「コード上のクラスの属性」を分割前の「ドメインモデル図上のクラスの属性」として返す
※ルールとして、値オブジェクトの属性を書き換えるメソッドは実装してはいけない。値オブジェクトの更新は、その値オブジェクトを代入している変数に、新規にインスタンス化した値オブジェクトを代入するようにする。
●メリット
「ドメインモデル図上のクラスの属性」は、プリミティブ型(StringとかIntとかFloatとか)で実装できるが、値オブジェクト化することで以下のメリットを享受できる。
実装項目 | メリット |
---|---|
プリミティブ型から独自クラスの型として拡張する | 「属性」をコード上で扱うときの変数、引数への代入ミスを検知できる(静的型付け言語だと特に) |
コンストラクタ内でのバリデーションの実装 | 「属性」に想定外の値をセットさせないようにできる (逆にこれをやらない場合、厳密性を担保するには、「属性に値をセットする」場面毎に、いちいちバリデーション処理を書いてやる必要がある) |
属性をパーツ分解した上で、値オブジェクト同士の四則演算・論理演算を実装 | 「値オブジェクトに対する四則演算・論理演算をしたい」場面で、「中身の属性を使った四則演算・論理演算のステートメントを複数書く必要がなくなる」 |
エンティティ
「ドメインモデル図上のクラス」 に相当するオブジェクト。
固有性を識別子で管理すべきオブジェクトである。
以下の「属性」「メソッド」を持つクラスを実装し、このクラスのオブジェクトとして扱う。
●属性
- 「ドメインモデル図上のクラス内の属性」を、「コード上のクラスの属性」とする。その型は「値オブジェクト」の型であるのが望ましい。
- 固有性を表す識別子は絶対に属性として実装する(じゃないとエンティティじゃない)
- 識別子など変更不可能な属性は「読み込み専用」で定義する
●メソッド
- コンストラクタ:
- 引数: 「ドメインモデル図上のクラス内の属性」群
- 内部処理:
- ①引数として受け取った「ドメインモデル図上のクラス内の属性」値同士のバリデーションチェック(論理チェック)
- ②エンティティクラスの属性群に、引数で受け取った値をセット
- 属性値を使った各種判断メソッド:
- エンティティとして同一かどうかの判断: 識別子である属性を使用
- 以下、その他の例
- (期間を属性として持つエンティティの場合)引数で指定した日時が対象エンティティの期間に含まれるか
- (ステータスを属性として持つエンティティの場合)対象エンティティが、引数で指定したステータスであるか
- (各)属性値の更新処理: 当然だが識別子である属性値の更新は行っちゃダメ
ドメインサービス
複数の「エンティティ」を考慮した処理を担当するオブジェクト。
※代表例. 一般的なWebサービスでは、ユーザーのメールアドレスは重複してアカウント登録できないものが多い。
この重複チェックをするには、全ユーザーのメールアドレスが必要になる。
こういった処理を「ドメインサービス」のクラスに実装する。
以下の「属性」「メソッド」を持つクラスを実装し、このクラスのオブジェクトを「ドメインサービス」として扱う。
●属性
- ドメインサービスが対象とするエンティティの「リポジトリ」
●メソッド
- コンストラクタ:
- 引数: ドメインサービスが対象とするエンティティの「リポジトリ」
- 内部処理: 属性のリポジトリに引数で受け取ったリポジトリをセット
- 複数エンティティの属性値を使った便利メソッド:
- 作成しようとしているエンティティの属性値に重複がないかの判断
- 引数: 重複チェック対象の属性
- 内部処理:
- 対象エンティティのリポジトリから引数で受け取った「重複チェック対象の属性」でエンティティの検索を依頼
- エンティティが見つかった: true(重複あり), 見つからなかった: false(重複なし) を返す
- 以下、その他の例
- (期間を属性として持つエンティティの場合)引数で指定した日時が期間に含まれるかエンティティすべてを取得
- 引数: 日時
- 内部処理:
- 対象エンティティのリポジトリから引数で受け取った「日時」を「期間」に含むエンティティの検索を依頼
- ヒットしない: null, ヒットした: エンティティの配列 を返す
- (ステータスを属性として持つエンティティの場合)引数で指定したステータスであるエンティティすべてを取得
- (期間を属性として持つエンティティの場合)引数で指定した日時が期間に含まれるかエンティティすべてを取得
- 作成しようとしているエンティティの属性値に重複がないかの判断
リポジトリ
対象のエンティティについて、そのエンティティのデータを永続的に保存するデータストアと以下のやり取りをするオブジェクト。
- エンティティをデータストアへ保存(追加・更新)
- エンティティをデータストアから削除
- エンティティをデータストアから検索
以下の「属性」「メソッド」を持つクラスを実装し、このクラスのオブジェクトを「リポジトリ」として扱う。
●属性
- なし
●メソッド
以下、データストアがRDBの場合で、SQLクエリでレコードの操作を行う場合
- エンティティをデータストアへ保存(追加・更新)
- 引数: エンティティ
- 内部処理:
- RDBと接続
- 以下の処理をするSQLを発行して、RDBへエンティティを保存
2.1. エンティティと紐づくレコード群を管理するテーブルで、対象エンティティを識別子で検索
2.2.A エンティティと紐づくレコードが存在する場合: ヒットしたレコードの識別子以外のカラムを引数で受け取ったエンティティの属性値で更新
2.2.B エンティティと紐づくレコードが存在しない場合: テーブルへ引数で渡されたエンティティをレコードとして追加
- エンティティをデータストアから削除
- 引数: エンティティ
- 内部処理:
- RDBと接続
- 対象エンティティと紐づくレコード群を管理するテーブルから、引数で受け取ったエンティティに紐づくレコードを削除するSQLを発行
- エンティティをデータストアから検索
- 引数: エンティティの属性値(検索キーにするもの)
- 内部処理:
- RDBと接続
- エンティティと紐づくレコード群を管理するテーブルで、引数で受け取ったエンティティの属性値がカラム値としてマッチするレコードを検索するSQLを発行
アプリケーションサービス
ドメインモデルでユースケースとして挙げた処理を実装するオブジェクト。
以下の「属性」「メソッド」を持つクラスを実装し、このクラスのオブジェクトを「アプリケーションサービス」として扱う。
●属性
- ユースケースに関連するエンティティのリポジトリ、ドメインサービス
●メソッド
- コンストラクタ:
- 引数: ユースケースに関連するエンティティのリポジトリ、ドメインサービス
- 内部処理: 属性のリポジトリ、ドメインサービスに引数で受け取ったリポジトリ、ドメインサービスをセット
- ユースケース毎のメソッド:
- 例. 「アカウントを登録する」の場合
- 引数: メールアドレス・パスワード・氏名・住所
- 内部処理:
- 受けとった引数の「メールアドレス・パスワード・氏名・住所」の値オブエジェクトのインスタンスをそれぞれ生成
- 1.の値オブジェクト群で「アカウント」エンティティのインスタンスを生成
- データストア登録済みの「アカウント」エンティティ群の中に、これから登録しようとしているメールアドレスと同一のものがないかチェック
- リポジトリに2. のエンティティの登録を依頼
- 例. 「アカウントを登録する」の場合
コントローラ
(MVCフレームワークでの開発 & 実現するアプリケーションが「API」であることを例にすると)
受けたリクエストの「リクエストパラメータ」と「HTTPメソッド」に応じた「アクション(メソッド)」に紐づくアプリケーションサービスのメソッドを実行し、予めクライアント側と合意しているフォーマットのレスポンスを返すオブジェクト。
※採用するフレームワークによって実装は異なるが、少なくともコントローラクラスで対象のアプリケーションサービスのメソッド実行が可能な状態に実装しないとダメ。
レイヤードアーキテクチャ
「エリック・エヴァンスのドメイン駆動設計」で登場する4層で構成されるアーキテクチャ。
以下、ドメインオブジェクトに登場するクラスがどの層に所属するか。
層 | 所属クラス | 補足 |
---|---|---|
ドメイン層 | - 値オブジェクト - エンティティ - ドメインサービス - リポジトリ(のインターフェース) |
- ドメインモデル図に登場する要素の実装、データストアとのやり取りを行うリポジトリの処理の概要部分のみ実装 |
アプリケーション層 | - アプリケーションサービス | ユースケースの実装 |
インフラ層 | - リポジトリ(の実装) | データストアとのやり取りを行うリポジトリの処理の具体的な実装 |
プレゼンテーション層 | - コントローラ | - |
以下、図には載せてない用語
集約
「会社」というエンティティと、「社員」というエンティティがあるとき、
1つの会社に複数の社員が所属するような関係になる。
つまり、「会社」は「社員」を「集約」している関係である。
コード的には、【「会社」クラス内に「社員」オブジェクト(またはID)の配列を持つ】形で実装する。
仕様
エンティティに対する判断処理を別クラスに切り出したもの。
「集約している側のエンティティが集約する別のエンティティ群に対して、その属性値も考慮した処理を行いたい」
場合に実装するとよいらしい。(この場合は、自身のエンティティ群を考慮した処理を実装するドメインサービスと似ている)
ファクトリ
エンティティの新規作成を担うオブジェクト。
エンティティ作成時のID採番にRDBのシーケンサを使いたいときなどリポジトリとのやり取りが発生してしまうときに使うといいパターン。
依存
あるクラスAが別クラスBを属性として持つとき、クラスAはクラスBに「依存」するという。
また、クラスAが別クラスaの実装である場合も、クラスAはクラスaに依存しており、この場合は特に「汎化」という
3. 参考
書籍: ドメイン駆動設計入門