今回読んだ本
ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本
※ 本記事は私自身の経験に基づく理解により執筆しております。そのため、一部解釈に誤りが含まれている可能性があります。予めご了承ください。
ドメインとは
自分の中で、DDDやらドメイン知識やら言葉が一人歩きしていたためまずは「ドメイン」についての認識を正しくしたいです。
ドメインは「領域」の意味をもった言葉です。ソフトウェア開発におけるドメインは、「プログラムを適用する対象となる領域」を指します。
私たちが開発するソフトウェアは誰かのドメインにおける問題を解決するためのものです。
例えば会計システムであれば、金銭や帳票などといった概念がドメインであり、私たちは問題を解決するためにそれらドメインと向き合う必要があります。
ドメイン駆動設計 ≠ ○○アーキテクチャ
本書を読む前に私は以下の勘違いをしていました。
「ドメイン駆動設計?あークリーンアーキテクチャとかの話ね」
間違いというのは行き過ぎかもしれませんが、本書では下記を否定しています。
- ドメイン駆動設計には○○アーキテクチャが必要である
- ○○アーキテクチャを取り入れればそれはドメイン駆動設計である
○○アーキテクチャと書きましたが、本書では下記の3つが解説されています
- レイヤードアーキテクチャ
- ヘキサゴナルアーキテクチャ
- クリーンアーキテクチャ
ドメイン駆動設計において、アーキテクチャは主役ではなく、開発者がドメインを捉えうまく表現することに集中させる手段です。
ドメイン駆動設計で大切なこと
ドメインエキスパートとの対話から本当に解決するものを見つける
人間は自分が欲しいものについて案外理解していない、同じように問題と思っているものが問題ではなく別の場所に根深い問題があることが往々にしてあります。
開発者はドメインエキスパートと対話をして、ドメインモデルを作り上げる必要があります。
ドメインモデルを作り上げる過程で、ドメインエキスパートと開発者の間でユビキタス言語を浸透させることがより深い洞察を得るためのポイントです。
問題を解決するソフトウェアを開発するためには、コードがモデルを通じてドメインへと繋がってる必要があります。
ドメインモデル

先の図の、ドメインとコードを繋いでいるドメインモデルは具体的になんなのでしょう。
明確な答えはありませんが、開発者とドメインエキスパートが対話をし作り上げた状態遷移図やER図、ドキュメントなどの成果物であると言えます。
また、ドメインモデルを作っていくモデリング手法として、
10分でわかるドメインモデリングではsudoモデリングが紹介されています。
sudo は下記の頭文字をとっています。
- システム関連図 (s)
- ユースケース図 (u)
- ドメインモデル図 (d)
- オブジェクト図 (o)
ドメイン駆動設計のパターン
下記は書籍に書いているパターンの一部抜粋となります。
1. 知識を表現するドメインオブジェクト
- 値オブジェクト
- エンティティ
- ドメインサービス
2. アプリケーション実現のためのパターン
- リポジトリ
- アプリケーションサービス
値オブジェクト
特徴
-
不変性
- 一度作成された値オブジェクトは変更されず、必要な場合は新たに作成します。
-
属性による定義
- 内包する値そのものが等価性を決定し、同一の値ならば交換可能です。
例: UserName
クラス
class UserName {
public readonly value: string;
constructor(value: string) {
if (!value || value.length < 3) {
throw new Error("ユーザー名は3文字以上でなければなりません。");
}
this.value = value;
}
}
- メリット:
- ルールを一箇所に集約し、DRY原則を促進できる
- プリミティブ型の曖昧さを排除し、意図が明確になる
- string型であるとしか分からないよりも、3文字以上等の制約があることがわかるメリットがあるということ
- デメリット:
- クラスが増えるため、初学者には理解が難しい場合がある
エンティティ
エンティティの定義は文脈によって異なります。ここでは、ドメイン駆動設計におけるエンティティを扱います。
特徴
-
同一性
- 一意のIDにより区別され、ライフサイクルを持つ
- 内部属性は変更可能ですが、IDが同じであれば同一のエンティティと見なされる
例: User クラス
class User {
private readonly id: number;
private userName: UserName;
constructor(id: number, userName: UserName) {
this.id = id;
this.userName = userName;
}
changeUserName(newUserName: UserName): void {
// 変更前の検証ロジックなどをここで実施可能
this.userName = newUserName;
}
}
- メリット:
- 一意性によりデータの整合性を確保できる
- 状態変化を管理することで、ライフサイクルに沿った処理が可能になる
- デメリット:
- 状態管理が複雑になり、変更履歴の管理などが必要になる場合がある
ドメインサービス
特徴
- ステートレス:
- 内部に状態を持たず、純粋な振る舞いのみを提供する
- 補助的な役割:
- エンティティや値オブジェクトに無理に含めると不自然な処理(例: 重複チェックなど)を集中管理する
例: UserService クラス
interface UserRepository {
findByUserName(userName: UserName): User | undefined;
save(user: User): void;
}
class UserService {
constructor(private userRepository: UserRepository) {}
isUserNameDuplicate(userName: UserName): boolean {
return this.userRepository.findByUserName(userName) !== undefined;
}
}
- メリット:
- 不適切なロジックをドメインオブジェクトから排除し、設計の明快さを保つ
全ての振る舞いをドメインサービスに集約すると、ドメインオブジェクトが「貧血」状態になりやすいため、エンティティに定義すべきものなどを適切に判断する
リポジトリ
特徴
- データ永続化の抽象化
- RDBMS、NoSQL、インメモリなど抽象化することでドメインサービスがインフラを意識しないようにする
- ドメインサービスからインフラの詳細(例: データベース接続)を隠蔽し、コードの可読性と保守性を向上させる
例: UserRepository インターフェース
interface UserRepository {
findByUserName(userName: UserName): User | undefined;
save(user: User): void;
}
- メリット:
- リポジトリを用いてデータ操作のレイヤーを抽象化することで、ドメインサービスがクリアになる
- 反対にリポジトリを用いないと、ドメインサービスにDBの接続や特定のインフラに依存したコードが増え、目的がぼやけてしまう
アプリケーションサービス
特徴
- アプリケーションのユースケースを満たすためのロジック
- ビジネスルール(例: 「ユーザーの重複禁止」)はドメインオブジェクトやドメインサービスに実装し、アプリケーションサービスはその調整役に徹する
- ドメインに紐づかないアプリケーション固有のロジック
例: UserApplicationService クラス
// アプリケーションサービスのコード例
class UserApplicationService implements IUserApplicationService {
constructor(
private userRepository: IUserRepository, // ドメイン駆動設計で定義済みのリポジトリインターフェース
private userService: IUserService // ドメインサービス(重複チェックなどを担当)
) {}
// ユーザー登録のユースケースを実現するメソッド
registerUser(userNameInput: string): UserDto {
// 入力値から値オブジェクトを生成
const userName = new UserName(userNameInput);
// ドメインサービスを用いてユーザー名の重複チェックを実施
if (this.userService.isUserNameDuplicate(userName)) {
throw new Error("ユーザー名が重複しています。");
}
// 新規ユーザーエンティティを作成(ユニークなIDを生成)
const newUser = new User(generateUniqueId(), userName);
// リポジトリを利用して新規ユーザーを永続化
this.userRepository.save(newUser);
// 登録結果をDTOに変換して返却(Userクラスには getId() と getUserName() を実装している前提)
return new UserDto(newUser.getId(), newUser.getUserName().value);
}
}
参考