- DDDでドメインを設計するときの流れ
- ドメイン設計の全体の流れ
- この記事の前提
- 1. ユースケースを書き出す
- 2. ドメイン用語を整理する(ユビキタス言語)
- 3. エンティティ候補を出す
- 4. 不変条件を書く
- 5. 状態遷移を整理する
- 6. 集約(Aggregate)を決める
- 7. エンティティの責務を設計する
- 8. ドメインサービスを定義する
- 9. リポジトリを定義する
- 10. 最後にUIを作る
- まとめ
- この流れで設計する際のテンプレート
DDDでドメインを設計するときの流れ
― 実践的な設計手順 ―
ドメイン駆動設計(DDD)を学び始めると、多くの人が次の疑問を持ちます。
- ドメイン設計は何から始めればいいのか
- 実際の開発ではどのような手順で設計するのか
DDDの本では概念の説明が多く、「実際の設計手順」がはっきりしないことがあります。
この記事では、実務で使える ドメイン設計の具体的な手順 を整理して紹介します。
ドメイン設計の全体の流れ
DDDでドメインを設計する場合、次の順序で進めると整理しやすくなります。
- ユースケースを書き出す
- ドメイン用語を整理する(ユビキタス言語)
- エンティティ候補を出す
- 不変条件を書く
- 状態遷移を整理する
- 集約(Aggregate)を決める
- エンティティの責務を設計する
- 必要ならドメインサービスを作る
- リポジトリを定義する
- 最後にUIを設計する
重要なのは UIから設計を始めないこと です。
DDDでは、基本的には、
ユースケース → ドメイン → UI
という順序で設計します。
実際には、一度でバシッと決められるわけではないため、ドメインを設計しているときに、ユースケースに戻るなど、各プロセス間を何度も行ったり来たりします。
この記事の前提
この記事では、口座振替管理アプリの開発を想定して、設計の流れを説明してきます。
振替元と振替先をユーザーが紐づけるメモアプリを想像してください。
1. ユースケースを書き出す
最初に、システムでユーザーが行う操作を書き出します。
ポイントは UIではなく行動を書くこと です。
【例】
- 口座を登録する
- 振替関係を作る
- 振替関係を停止する
- 振替予定を確認する
ここでは 動詞を使って定義する ことが重要です。
この一覧が、アプリケーションのユースケースになります。
2. ドメイン用語を整理する(ユビキタス言語)
次に、ドメインで使う重要な言葉を整理します。
【例】
- 口座
- サービス
- 振替関係
- 振替予定
- 支払日
ここでやることは次の2つです。
- 同じ意味の言葉を統一する
- UIの言葉をそのまま使わない
例えば次のような状態は避けます。
【NG 例】
- 支払い元
- 銀行
- 口座
これらは意味が同じである場合、例えば 口座 に統一します。
DDDでは、このようにチーム全体で共通の言葉を使うことを ユビキタス言語 と呼びます。
3. エンティティ候補を出す
次に、システムの中心になる概念を探します。
【例】
- Account
- Service
- TransferRelation
- TransferSchedule
この段階では、まだ厳密に決める必要はありません。 重要なのは ドメインの主要な概念を見つけること です。
4. 不変条件を書く
DDDで最も重要なのが 不変条件(Invariant) です。
不変条件とは
どんな操作をしても絶対に破ってはいけないルール
です。
【例】
- 振替関係は必ず1つの口座を持つ
- 振替関係は必ず1つのサービスを持つ
- 振替日は1〜31の範囲
- 「口座とサービス」が同じ組み合わせの振替関係は、重複できない
このように 箇条書きで書く のがポイントです。
実際の設計では、この不変条件がシステム仕様の大部分を決めます。
5. 状態遷移を整理する
状態を持つエンティティについては 状態遷移 を整理します。
【例:振替関係】
作成
↓
有効
↓
停止
↓
削除
さらに重要なのは
どの操作で状態が変わるのか
です。
【例】
- create → 有効
- stop → 停止
- resume → 有効
- delete → 削除
この整理をしておくことで、 不正な状態遷移を防ぐことができます。
6. 集約(Aggregate)を決める
DDDでは、不変条件を守る単位を 集約(Aggregate) と呼びます。
特に、複数のエンティティをまたぐ不変条件は、それらを束ねる集約で定義します。
【例】
TransferRelation
├ AccountId
├ ServiceId
└ paymentDay
このエンティティは次のルールを守ります。
「口座 + サービス」が同じ組み合わせの振替関係は 1 つだけ (重複不可)
このルールを守る境界が 集約 です。
7. エンティティの責務を設計する
次に、エンティティのドメインメソッドを設計します。
【例】
class TransferRelation {
stop()
resume()
changePaymentDay()
}
ここで重要なのは setterを作らないこと です。
【NG】
setPaymentDay()
【OK】
changePaymentDay()
では、なぜ setter は避けるべきなのでしょうか。
setterが問題になる理由
setPaymentDay() のような setter は、単に値を代入するだけのメソッドです。
transferRelation.setPaymentDay(50)
このコードは 技術的には成立してしまいます。
しかし、ドメインのルールには次のものがあります。
- 振替日は1〜31の範囲
つまり、setPaymentDay() を許すと
- 不正な値が設定できてしまう
- 不変条件が破られる
という問題が起こります。
ドメインメソッドは「操作」を表す
DDDでは、エンティティのメソッドは 単なる値変更ではなく、ドメインの操作を表す べきです。
changePaymentDay(newDay)
という名前にすると
- 支払日を変更するという操作
- その操作にはルールがある
という意味を持たせることができます。
例えば次のように実装できます。
fun changePaymentDay(newDay: Int) {
require(newDay in 1..31)
paymentDay = newDay
}
このように 不変条件のチェックをエンティティの中に閉じ込める ことができます。
重要な考え方
DDDでは、エンティティは単なるデータ構造ではありません。
エンティティはビジネスルールを守る存在です。
そのため
setXxx()のような単純な setter- 外部から自由に状態を変更できる設計
は避けます。
代わりに
changePaymentDay()stop()resume()
のように ドメインの操作を表すメソッド を設計します。
これによって
- 不変条件が守られる
- ドメインモデルが壊れにくくなる
- コードから業務の意味が読み取れる
というメリットが生まれます。
8. ドメインサービスを定義する
エンティティに入らない処理は ドメインサービス にします。
無理に作成する必要はなく、必要な場合のみ作成します。
【例】
TransferExecutionService
【責務】
- 今日振替すべきものを取得する
- 振替を実行する
エンティティが持つべきでない処理をここに配置します。
9. リポジトリを定義する
永続化のためのインターフェースです。
【例】
- TransferRelationRepository
- AccountRepository
- ServiceRepository
リポジトリは ドメイン層にインターフェースだけ定義 します。
10. 最後にUIを作る
ここまで終わって、初めてUIを設計します。
多くのアプリは次の順序で設計されます。
UI
↓
ViewModel
↓
Repository
↓
DB
しかし、DDDでは次の順序になります。
ユースケース
↓
ドメイン
↓
UI
この順序にすると
- UI変更の影響を受けない
- ビジネスルールが守られる
- 設計が崩れにくい
というメリットがあります。
まとめ
DDDでドメイン設計をする際は、次の手順で進めると整理しやすくなります。
- ユースケースを書く
- ドメイン用語を整理する
- エンティティ候補を出す
- 不変条件を書く
- 状態遷移を整理する
- 集約を決める
- エンティティの責務を設計する
- ドメインサービスを定義する
- リポジトリを定義する
- 最後にUIを設計する
DDDの設計で最も重要な問いは次のものです。
「このルールはどこで守るのか?」
この問いを繰り返すことで、ドメインモデルは徐々に洗練されていきます。
この流れで設計する際のテンプレート
この流れで設計する際のテンプレートを ドメイン設計テンプレート に用意しました。