domain-driven-design について
データ指向設計と型駆動設計を組み合わせた、開発者向けのドメインモデリングスキルです。無効な状態を表現しにくくし、コードとビジネス用語を整合させます。
主な機能
- 01データ指向のモデリング
- 02型駆動のドメイン設計
- 03明示的なワークフローとエラー経路
- 04DDDの構成要素と境界
- 05ドメイン図と用語集のガイド
活用シーン
- 01不変データによる業務ドメインのモデル化
- 02型による有効な状態と遷移の表現
- 03ドメインロジックとインフラの分離
- 04ドメイン用語と関係の文書化
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ bfollington/ terma --skill domain-driven-designインストール後、Claude Code で/domain-driven-designを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/ddd_foundations_and_patterns.mdrich_hickey_principles.mdvisualization_examples.mdwlaschin_patterns.md詳細
ドメイン駆動設計
概要
このスキルでは、Rich Hickey のデータ指向設計の原則と Scott Wlaschin の型駆動設計のアプローチに基づき、ドメインモデリングの指針を提供します。不正な状態を表現できないシステムの構築、オブジェクトやメソッドよりもデータと変換を優先すること、技術的な実装とビジネスドメインをつなぐユビキタス言語の確立に重点を置きます。
基本原則
Rich Hickey のデータ指向設計
容易さより単純さ
- 個別に理解できる単純な構成要素を優先します
- 無関係な関心事を絡み合わせること(コンプレクシング)を避けます
- ポリシーとメカニズム、データと振る舞いを分離します
データが重要
- 振る舞いを持つオブジェクトではなく、純粋なデータ構造を使ってドメインをモデル化します
- 適切な場合は、カスタムクラスより汎用データ構造(マップ、セット、ベクター)を優先します
- データは自己記述的で、検査できるものにします
- 関数はデータを変換します。データが振る舞いを実行することはありません
値の価値
- 事実を表すために不変の値を使用します
- 値を使うと、局所的な推論と単純な等価性の判定が可能になります
- 値は調整なしに自由に共有できます
- 検討事項:このドメインにおける不変の事実は何でしょうか?
複雑さの分離
- ドメインにとって本質的なものと、偶発的な複雑さを見極めます
- いつ起きるかと何が起きるかを分離します
- メカニズムとポリシーを分離します
- 問いかけ:これらの関心事は本当に別々でしょうか。それとも絡み合わせてしまっているでしょうか?
Scott Wlaschin の型駆動設計
不正な状態を表現できないようにする
- 型システムを使って、コンパイル時に無効な状態を排除します
- オプショナルな値を明示的にモデル化します(Option/Maybe 型)
- 相互に排他的な状態には、直和型(判別共用体)を使用します
- プリミティブ型への固執を避け、ドメイン固有の型を作成します
関数型によるドメインモデリング
- ワークフローをデータ変換としてモデル化します:入力 → 処理 → 出力
- ビジネスルールを関数として明示的にモデル化します
- 検証とビジネスロジックを分離します
- 次の観点で考えます:何が起こり得るか。有効な遷移は何か。
レール指向プログラミング
- 成功と失敗の経路を明示的にモデル化します(Result 型)
- bind/flatMap を使って、失敗する可能性のある操作を連結します
- 正常系の経路を明確かつ直線的に保ちます
- 適切な境界でエラーを処理します
ドキュメントとしての型
- 型シグネチャで意図が伝わるようにします
- 意味を明確にするために newtype ラッパーを使用します(UserId、EmailAddress、Timestamp)
- 型を使って入力を有効な範囲に制限します
- 型システムを API 設計の指針にします
DDD の構成要素
エンティティと値オブジェクト
エンティティは属性ではなく、アイデンティティによって定義されます:
- 一意の識別子(ID、口座番号など)を持ちます
- アイデンティティを維持したまま、時間の経過とともに変化します
- 属性が同じでも ID が異なる 2 つのエンティティは、別個のものです
- ドメインエキスパートが名前や ID で呼ぶ対象に使用します
値オブジェクトは属性のみによって定義されます:
- 一意の識別子を持ちません
- 不変です
- 属性が同じ 2 つの値オブジェクトは、互いに置き換え可能です
- アイデンティティではなく、値だけが重要な場合に使用します
判断の指針:
- 問いかけ:ドメインエキスパートはこれを ID や名前で呼ぶでしょうか? → エンティティ
- 問いかけ:同等の複製で置き換えられるでしょうか? → はいの場合:値オブジェクト
集約と集約ルート
集約:データ変更において、単一の単位として扱われるエンティティと値オブジェクトのまとまりです。
集約ルート:集約への外部からのアクセスがすべて経由しなければならない、唯一のエンティティです。
目的:
- トランザクションの整合性境界を定義する
- 複数のオブジェクトにまたがる不変条件を適用する
- 関連する概念をまとめて、モデルをシンプルにする
ルール:
- 外部からの参照先は集約ルートのみとします(ID 参照を使用します)
- 集約ルートが集約全体のすべての不変条件を適用します
- トランザクションは集約境界をまたぎません(結果整合性を使用します)
- パフォーマンスとスケーラビリティを高めるため、集約は小さく保ちます
集約を作成しない場合:
- オブジェクトを個別に変更できる
- トランザクション整合性を必要とする共通の不変条件がない
- オブジェクトごとにライフサイクルが異なる
境界づけられたコンテキスト
定義:ドメインモデルが適用される、明確な境界です。
目的:
- 大きなドメインを管理しやすい単位に分割する
- 同じ用語がコンテキストごとに異なる意味を持つことを可能にする
- 互換性のない概念が混在してモデルが破損するのを防ぐ
重要なポイント:ユビキタス言語が共有されるのは、コンテキスト内に限られます。営業コンテキストの「顧客」と配送コンテキストの「顧客」は、異なる場合があります。
モデリング時の注意点:
- どの境界づけられたコンテキストにいるかを特定します
- コード構造(個別のモジュール/名前空間)でコンテキスト境界を明示します
- コンテキスト間の連携には腐敗防止層を使用します
- コンテキスト間の関係を文書化します(コンテキストマップ)
ドメインイベント
定義:ドメインで起きた重要な出来事です。
特徴:
- 過去形の名前を付けます(OrderPlaced、PaymentProcessed、UserRegistered)
- 不変の事実です
- ドメインエキスパートにとって重要です
- 境界づけられたコンテキスト内またはコンテキスト間で反応を引き起こすことがあります
用途:
- ドメインロジックを疎結合にする
- 集約間の結果整合性を実現する
- 境界づけられたコンテキスト間の連携
- イベントソーシング(イベントを信頼できる唯一の情報源として保存する)
リポジトリ
目的:永続化を抽象化し、集約のインメモリコレクションを扱っているように見せます。
特徴:
- 集約境界で操作します(集約全体を読み込み/保存します)
- ID による検索を提供します
- データベースの実装詳細を隠蔽します
- データベースの行ではなく、ドメインエンティティを返します
パターン:アプリケーション層はリポジトリを使用して集約を取得/保存し、ドメイン層は純粋な状態を保ちます。
ドメインモデリングのワークフロー
1. ユビキタス言語を見つける
ドメインエキスパートが使う用語を用いて、まずドメインの概念を特定します。
作業項目:
- ドメインに関する名詞(エンティティ、値オブジェクト)と動詞(操作、イベント)を列挙します
- ドメイン用語を正確な定義とともに文書化します
- 同義語を特定し、曖昧さを解消します
- 次のように尋ねます。この概念をビジネスでは何と呼んでいますか?この概念の境界はどこですか?
出力形式: 各用語を記載した用語集のセクションを作成します:
**Term** (Type: Entity/ValueObject/Event/Command)
- Definition: [Clear, domain-expert-approved definition]
- Examples: [Concrete examples]
- Invariants: [Rules that must always hold]
2. 既存のドメインモデルを分析する
変更を加える前に、現状を把握します。
調査手順:
- 現在、ドメイン概念がどこでモデル化されているかを特定します(型、スキーマ、テーブル)
- ドメインエンティティ間の関係を整理します
- ビジネスロジックがどこにあるかを調べます(サービス、関数、ストアドプロシージャ)
- 暗黙のルールや制約を文書化します
- 命名やモデリングの不整合を記録します
確認する質問:
- どの型/クラスがドメイン概念を表していますか?
- 不変条件は何ですか?どこで適用されていますか?
- 分離すべきなのに、どの概念が絡み合っていますか?
- 存在すべきでないファントム型や状態はありますか?
3. 不整合や問題点を特定する
よく見られる問題:
命名の不整合
- 同じ概念に異なる名前が使われている(User、Account、Customer)
- 異なる概念に同じ名前が使われている(エンティティとしてのOrderとコマンドとしてのOrder)
- 技術的な名前がドメイン用語に入り込んでいる(DTO、DAOの接尾辞)
構造上の問題
- 不正な状態を表現できてしまう(例:
status: "approved" | "rejected"に加えてapproved_atとrejected_atのフィールドがあり、両方を設定できる) - プリミティブ型への執着(メールアドレスに文字列、金額に数値を使う)
- 特定の状態では必須なのに、オプションになっているフィールド
- 複数の異なる状態を表すために、Null/undefinedを使用している
関心事の混在
- ドメインロジックとインフラストラクチャが混在している(ビジネスロジック内でのDBアクセス)
- 1つの型/モジュールに複数の責務がある
- 時間的結合(Aの後にBを呼び出さないと、システムが壊れる)
概念の欠落
- 会話には登場するが、コードには存在しないドメイン概念
- 明示すべき暗黙の状態
- 型ではなく、コメントや規約によって適用されているビジネスルール
4. ドメインモデルを設計する
型駆動とデータ駆動の原則を適用します。
データモデリング:
- データの形から始めます。事実は何でしょうか?
- 変化しない事実には、不変の値を使用します
- 状態遷移を明示的にモデル化します
- 識別子と属性を分離します
- 次の点を検討します。同時に変化するものは何ですか?独立して変化するものは何ですか?
型の設計:
- 相互排他的な状態には、和型を作成します。
type PaymentStatus = | Pending | Approved { approvedAt: Timestamp, approvedBy: UserId } | Rejected { rejectedAt: Timestamp, reason: string } - 必須データがすべて揃うように、積型を使用します
- プリミティブ型を意味のあるラッパーで包みます。
type EmailAddress = EmailAddress of string // with validation type Money = { amount: Decimal, currency: Currency } - 不可能な状態を表現できないようにします
ワークフローのモデリング:
- 各ビジネスワークフローを明確なパイプラインとしてモデル化します。
ValidateInput → ExecuteBusinessLogic → HandleResult → Persist → Notify - 判断点を特定し、明示的にモデル化します
- 純粋なビジネスロジックを副作用(IO、時刻、乱数)から分離します
- 意図が伝わる明確な関数シグネチャを使用します
5. ユビキタス言語を構築し、維持する
一貫性のルール:
- コード、ドキュメント、会話、UIで同じ用語を使用します
- ドメイン用語が変わったら、すべての表現を更新します
- ドメインコードで技術用語を使わないようにします(ドメイン用語でない限り、「factory」、「manager」、「handler」などは使いません)
- 技術的な都合でドメイン概念の名前を変更する誘惑に負けないようにします
コード規約:
- ドメイン型には、ドメイン用語をそのまま反映します
- 関数名には、ドメインの動詞を使用します
- モジュールの境界は、ドメインの境界に沿わせます
- コメントでは実装の詳細ではなく、ドメインルールを説明します
ドキュメント:
- 用語集を最新の状態に保ちます
- 決定した理由(特に制約や不変条件)を記録します
- コードからドメインドキュメントにリンクします
- 暗黙のドメインルールを明示します
6. ドメインモデルを可視化する
図を使って、ドメインの構造と関係を伝えます。
関係を表すMermaid:
classDiagram
Order --> Customer
Order --> OrderLine
OrderLine --> Product
Order --> PaymentStatus
class Order {
+OrderId id
+CustomerId customerId
+List~OrderLine~ lines
+PaymentStatus status
}
class PaymentStatus {
<<enumeration>>
Pending
Approved
Rejected
}
ワークフローを表すMermaid:
graph LR
A[Receive Order] --> B{Valid?}
B -->|Yes| C[Calculate Total]
B -->|No| D[Return Validation Error]
C --> E[Process Payment]
E --> F{Payment Success?}
F -->|Yes| G[Fulfill Order]
F -->|No| H[Cancel Order]
状態遷移を表すMermaid:
stateDiagram-v2
[*] --> Draft
Draft --> Submitted: submit()
Submitted --> Approved: approve()
Submitted --> Rejected: reject()
Approved --> Fulfilled: fulfill()
Fulfilled --> [*]
Rejected --> [*]
複雑な関係を表すGraphviz/DOT:
digraph domain {
rankdir=LR;
node [shape=box];
Customer -> Order [label="places"];
Order -> OrderLine [label="contains"];
OrderLine -> Product [label="references"];
Order -> Payment [label="requires"];
Payment -> PaymentMethod [label="uses"];
}
手早いスケッチに使うASCII:
Customer
└─> Order (1:N)
├─> OrderLine (1:N)
│ └─> Product
└─> Payment (1:1)
└─> PaymentMethod
使い分け:
- Mermaid classDiagram:エンティティ間の関係と型構造
- Mermaid graph/flowchart:ビジネスワークフローと判断ツリー
- Mermaid stateDiagram:状態遷移とライフサイクル
- Graphviz/DOT:複雑な依存関係グラフ、モジュール境界
- ASCII:議論中の手早いスケッチ、単純な階層構造
ドメインモデリングのアンチパターン
貧血ドメインモデル
- 症状:ゲッター/セッターを備えたデータ構造で、すべてのロジックが別のサービスにある
- 問題:カプセル化の利点がないまま儀式的なコードを増やし、データ指向に反します
- 解決策:データはデータとして保ち、関連する変換処理はデータ定義とは分けつつ、同じモジュールに置きます
エンティティサービスのアンチパターン
- 症状:
UserService、OrderManager、ProductFactoryのようなクラス - 問題:実際の操作が隠れ、ユビキタス言語が不足しています
- 解決策:関数にはドメイン操作に基づいた名前を付けます。例:
approveOrder、cancelSubscription、calculateDiscount
プリミティブ型への執着
- 症状:メールアドレスを文字列、金額を数値、状態をブール値のフラグで表します
- 問題:型安全性がなく、無効な値を表現できてしまいます
- 解決策:検証機能を備えた、意味のある型を作成します
偶発的な複雑さ
- 症状:ドメイン上の明確な利点がない複雑な抽象化やデザインパターン
- 問題:ドメインの意味を見えにくくするレイヤーが増えます
- 解決策:シンプルにし、継承よりもコンポジションを優先し、早すぎる抽象化を避けます
隠れた時間的結合
- 症状:特定の順序でメソッドを呼び出さなければ、システムが壊れます
- 問題:ワークフローと状態管理が複雑に絡み合います
- 解決策:ワークフローを明示し、型を使って有効な遷移を強制します
ブール値の多義性
- 症状:状態を表すためにブール値のフラグを使います(isApproved、isActive、isDeleted)
- 問題:複数のブール値によって、あり得ない状態を表現できてしまいます
- 解決策:相互に排他的な状態には、直和型を使います
既存モデルにおける位置づけ
既存のドメインモデルに追加または変更を加える場合:
- 現状を把握する:既存の型、関係、パターンを文書化します
- 影響を受ける概念を特定する:この変更は既存のどの概念に影響しますか?
- 一貫性を確認する:新しい設計は既存のパターンに従っていますか?従っていない場合、その理由は何ですか?
- 影響を評価する:この変更によって何が壊れますか?
- 移行経路を定める:現状から目指す状態へ、どのように進化させますか?
- ユビキタス言語を更新する:すべての使用箇所が更新されていることを確認します
- 変更前後を可視化する:現状と提案するモデルを示す図を作成します
重要な質問:
- この変更は既存のドメイン境界に沿っていますか?
- 用語を一貫して使っていますか?
- 新しい概念を導入しますか、それとも既存の概念を再利用しますか?
- 不整合を修正しますか、それとも新たな不整合を生み出しますか?
- この変更を段階的に進められますか?
ドメインモデリングのチェックリスト
ドメインモデリング作業を完了する前に、以下を確認してください。
言語とコミュニケーション:
- すべてのドメイン概念に、ユビキタス言語を用いた名前が付けられている
- ドメイン用語集が、新規または変更された用語に合わせて更新されている
- コード、ドキュメント、会話で同じ用語が使われている
- 境界づけられたコンテキストが明確に特定され、ドキュメント化されている
型の設計:
- 型によって不正な状態を表現できないようになっている
- プリミティブ型への執着がなく、意味を持つ型が適切に使われている
- エンティティの識別子が明確で、値オブジェクトが不変になっている
- 相互に排他的な状態に代数的データ型が使われている
ドメインロジック:
- ビジネスルールが明示され、テスト可能になっている
- データと振る舞いが適切に分離されている
- ワークフローが明確なデータ変換としてモデル化されている
- ドメインロジックが純粋である(副作用がない)
- 時間的結合が排除されているか、明示されている
集約と境界:
- 集約の境界が明示されている
- 集約が不変条件を守っている
- 集約への外部からの参照には ID のみが使われている
- 集約が小さく、目的に集中している
- トランザクション境界が適切である
整合性と統合:
- 既存のモデルとの不整合が解消されているか、文書化されている
- 集約間の整合性戦略(トランザクション整合性と結果整合性)が定義されている
- 重要な出来事にドメインイベントが使われている
- 境界づけられたコンテキスト間の統合に腐敗防止層が使われている
ドキュメント:
- 可視化図によって設計が明確に伝わる
- 重要な決定事項と不変条件が文書化されている
- コンテキストマップに境界づけられたコンテキスト間の関係が示されている
リソース
references/
このスキルには、詳しく調べるためのリファレンスドキュメントが含まれています。
-
ddd_foundations_and_patterns.md:Eric Evans による DDD の基礎概念(エンティティ、値オブジェクト、集約、境界づけられたコンテキスト、リポジトリ、ドメインイベント)、Martin Fowler によるユビキタス言語のガイダンス、実践的な Clojure/関数型パターンを解説します。DDD の構成要素とその適用方法を理解するうえで必読の資料です。
-
rich_hickey_principles.md:Simple Made Easy、Value of Values、The Language of the System など、Rich Hickey の講演で扱われた主要な概念を解説します。データ指向設計、シンプルさ、複雑な要素の分離、不変値の力に焦点を当てています。
-
wlaschin_patterns.md:Scott Wlaschin による型駆動設計のパターン、ドメインモデリングのレシピ、関数型アーキテクチャのガイダンス、レールウェイ指向プログラミングを解説します。不正な状態を表現できないようにすることと、型を使った設計を重視しています。
-
visualization_examples.md:ドメインモデリングにおける Mermaid、Graphviz、ASCII 図のパターンを幅広く紹介します。エンティティの関係、ワークフロー、状態機械、集約の境界、境界づけられたコンテキストのマップを含みます。
特定の原則やパターンについて詳しい情報が必要な場合は、これらのリファレンスを読み込んでください。