ドメイン駆動設計の個人的な理解
導入
日々、業務でドメイン駆動設計を使用してお仕事をしているのですが、理解が中途半端であったり理解したふりをしている箇所があることに気がつきました。
そこでもう一度ドメイン駆動設計入門を読み直してみたので、その理解を記事にまとめておこうと思います。
ドメイン駆動設計構成をする要素
ドメイン駆動設計を構成する要素として以下のようなグループに分類される。
- 知識を表現するパターン
- 値オブジェクト
- エンティティ
- ドメインサービス
- アプリケーションを実現するパターン
- リポジトリ
- アプリケーションサービス
- ファクトリ
- 知識を表現する、より発展的なパターン
- 集約
- 仕様
知識を表現するパターン
- ドメイン(業務知識)をコード化したい
- そのためにドメインオブジェクトと呼ばれるオブジェクトを使って表現するためのグループ
アプリケーションを実現するパターン
- ドメインオブジェクトを用意するだけではドメインをコード化しただけ
- ドメインオブジェクトを使用してアプリケーションとして利用者の要求を満たすためのグループ
知識を表現する、より発展的なパターン
- より発展的に知識を表現するためのグループ
値オブジェクト
値オブジェクトには以下のような特徴がある。
- 不変
- 交換が可能
- 等価性によって評価される
不変
一度作成されたオブジェクトは中身が変更されてはいけないというルール。
変更を許可してしまうと、別名参照問題を引き起こしてしまう可能性がある。
交換が可能
新しく作成される場合は代入するのではなく、新しくインスタンス化することによって再生成する必要がある。
$user = new User("Yamada", "Taro");
$user = new User("Suzuki", "Jiro"); // 新しく作り直す
個人的には「交換が可能」ではなく「交換しなくてはいけない」と解釈しています。
等価性によって評価される
値が同じであれば同じものだとみなします。
$user1 = new User("Yamada", "Taro");
$user2 = new User("Yamada", "Taro");
// オブジェクト同士の比較で等価性を評価する
if ($user1 == $user2) reutrn true
値オブジェクトにする基準
- 場合による
- その値についてルールがそんざいしているか
- 単体で扱いたいか
オブジェクトの振る舞いについて
値オブジェクトには振る舞いを定義することができる。
ここにドメイン知識やそれを実現するためのロジックが集まってくるイメージ。
以下はPriceオブジェクトの例
class Price()
{
// 税込価格を返す
public function taxIncludingPrice(float $price): float
{
return $price * 1.1;
}
}
値オブジェクトのメリット
- 表現力が増す
- 不正な値を存在させない
- 誤った代入を防ぐ
- ロジックの散在を防ぐ
エンティティ
- 可変である
- 同じ属性であっても区別する
- 同一性によって区別される
可変である
オブジェクトが持っている値は後から変更することができる
class User
{
private $name;
// 名前を変更する
public function changeName(string $name): void
{
// 必要に応じてバリデーションなどを入れる
$this->name = $name;
}
}
同じ属性であっても区別される
同じ属性(名前や苗字など)を持っていても別のものと区別される
ただし、それぞれを識別するための識別子(IDなど)が必要になる
class User
{
private $id; // 識別子を使用して区別される
private $name;
private function setId(int $id): void
{
$this->id = id;
}
private function setName(string $name): void
{
$this->name = $name;
}
}
同一性によって区別される
オブジェクトが持つ値が変更されても同じものとして扱われる
class User
{
private $id;
private $name;
private function setId(int $id): void
{
$this->id = id;
}
private function setName(string $name): void
{
$this->name = $name;
}
public function changeName(string $name): void
{
$this->name = $name; // 名前が変更されても識別子が同一のため同じものと判断される
}
}
エンティティにする判断基準
- ライフサイクルがある場合はエンティティとして取り扱う
- ライフサイクルのあるデータは内容が変更されることが予想される
- それ以外は一旦値オブジェクトとして扱う
ドメインオブジェクトを定義するメリット
- コードのドキュメント性が高まる
- ドメインの変更をコードに伝えやすくする
コードのドキュメント性が高まる
ロジックがドメインオブジェクト内に集まっているため、他の人がそのコードを読んだときにこコードの意図を汲み取りやすく保守がしやすい
ドメインの変更をコードに伝えやすくする
ドメインオブジェクトにルールや振る舞いが集まっているため、ドメイン(業務知識、業務仕様)が変更になった場合もどの変更をコードに反映しやすい
ドメインサービスとは
- ドメインオブジェクトに定義するべきではない処理を記述する
- 状態を持たないオブジェクト(ステートレス)
class UserDomainService
{
// Userの重複を確認する
// ドメインオブジェクトにあるべきではない処理
public function exists(User $user)
{
// 重複を確認するコード
}
}
リポジトリとは
- オブジェクトのデータを永続化・復元を行う
- そのためにDBとのやり取りをする責務を負う
インターフェースの利用
インターフェースを用意してあげることで例えばDBがMySQLでもElasticSearchでも切り替えやすくなる(依存性の逆転)
リポジトリに定義される振る舞い
- データの永続化(保存)
- データの再構築(取得)
データの永続化(保存)
SaveメソッドのようなDBへのデータの永続化を行う。削除系の処理もリポジトリの責務。
ただし更新系の処理はリポジトリの責務ではない。オブジェクトが保存するデータを変更する場合は、オブジェクト自身がその仕事をするべき。
データの再構築(取得)
識別子を利用して検索を行う。
class UserRepository implements IUserRepository
{
public function getById(int $id): User
{
return DB::table('Users')->where('id', $id)->get();
}
}
アプリケーションサービス
- アプリケーションの要求を実現するためにユースケースを組み立てる役割
- 会員登録
- 会員情報確認
- 会員情報更新
- 退会
- クライアントとしてドメインオブジェクトを操作する
- エンティティや値オブジェクト、ドメインサービスの振る舞いを呼び出す
集約
- 不変条件を切り出す単位
集約の基本的構造
集約の外から内部のオブジェクトを操作してはいけない。操作するのは集約ルートに限定される。つまりその集約が保持しているデータの変更はそのオブジェクト自体が行わなくてはいけない。そのように操作することで内部の不変条件を保つことができる。
オブジェクト操作に関する基本的な原則
不変条件を保持するための「デメテルの法則」ではメソッドを呼び出すことができるのは以下の4つ
- オブジェクト自身
- インスタンス変数
- 引数として渡されたオブジェクト
- 直接インスタンス化したオブジェクト
集約をどう区切るのか
- 最もメジャーな単位は「変更の単位」
変更の単位
もしある集約(ex Circle集約, User集約)が他の集約を操作する処理を保持していた場合、そのほとんどが他の集約を操作する処理で汚染されてしまう。
集約に対する変更はその集約自信が担当する必要があり、永続化の依頼も集約ごとに行われる必要がある。このような理由によりリポジトリは変更の単位である集約ごとに用意されることが必要である。
実装してみた
本を読んでみて自分の理解を深めるために実際にコードに落とし込んでみました。(とりあえずユーザ取得だけ)
コードはここに置いてあるので興味がある方はご参照ください
TODO: 解説書く
TODO: insertやupdateも実装する
Discussion