ドメイン駆動設計(DDD)関係の情報を漁っていると、次の3つの概念がごちゃごちゃになることがよくあります。
- Entity(エンティティ)
- ValueObject(値オブジェクト)
- DTO(Data Transfer Object)
どう定義するのか迷わない様に残しておきます。
Entity(エンティティ)
特徴
- ID(識別子)を持つ: 同じ属性値を持っていても、IDが異なれば別のオブジェクトとして扱う
- 可変性: 時間の経過とともに状態(プロパティ)が変化することがある
- ライフサイクルを持つ: 作成、変更、削除などのライフサイクルがある
- ビジネスルールをカプセル化: ドメインのビジネスルールや振る舞いを含む
例:商品を表すProductエンティティ
class Product
{
private ProductId $id;
private string $name;
private Money $price;
private int $stockQuantity;
public function __construct(ProductId $id, string $name, Money $price, int $stockQuantity)
{
$this->id = $id;
$this->name = $name;
$this->price = $price;
$this->stockQuantity = $stockQuantity;
}
public function getId(): ProductId
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function getPrice(): Money
{
return $this->price;
}
public function getStockQuantity(): int
{
return $this->stockQuantity;
}
public function changePrice(Money $newPrice): void
{
// 価格変更に関するビジネスルール
// 例:最低価格のチェックなど
$this->price = $newPrice;
}
public function isInStock(): bool
{
return $this->stockQuantity > 0;
}
public function decreaseStock(int $quantity): void
{
if ($quantity <= 0) {
throw new \InvalidArgumentException('数量は正の値でなければなりません');
}
if ($quantity > $this->stockQuantity) {
throw new \RuntimeException('在庫不足です');
}
$this->stockQuantity -= $quantity;
}
public function equals(Product $other): bool
{
// IDのみで同一性を判断
return $this->id->equals($other->id);
}
}
ValueObject(値オブジェクト)
特徴
- ID(識別子)を持たない: 同じ属性値を持つ二つのValue Objectは等価と見なされる
- 不変性(イミュータブル): 一度作成されると変更されない
- 副作用がない: メソッドは新しいインスタンスを返すことはあっても、自身の状態を変更しない
- 概念的な完全性: それ自体で完結した概念を表す
- 交換可能性: 同じ値を持つものなら互いに交換可能
例:金額を表すMoneyクラス
final class Money
{
private float $amount;
private string $currency;
public function __construct(float $amount, string $currency)
{
// バリデーション
if ($amount < 0) {
throw new \InvalidArgumentException('金額は0以上でなければなりません');
}
if (empty($currency)) {
throw new \InvalidArgumentException('通貨が指定されていません');
}
// 金額は小数点以下2桁までに正規化
$this->amount = round($amount, 2);
$this->currency = $currency;
}
public static function yen(float $amount): self
{
return new self($amount, 'JPY');
}
public static function dollar(float $amount): self
{
return new self($amount, 'USD');
}
// 新しいインスタンスを返す演算メソッド(イミュータブル)
public function add(Money $money): self
{
if ($this->currency !== $money->currency) {
throw new \InvalidArgumentException('通貨単位が異なります');
}
return new self($this->amount + $money->amount, $this->currency);
}
public function multiply(int $multiplier): self
{
return new self($this->amount * $multiplier, $this->currency);
}
public function isGreaterThan(Money $other): bool
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException('通貨単位が異なります');
}
return $this->amount > $other->amount;
}
public function getAmount(): float
{
return $this->amount;
}
public function getCurrency(): string
{
return $this->currency;
}
public function equals(Money $other): bool
{
// 金額と通貨の両方で等価性を判断
return $this->amount === $other->amount &&
$this->currency === $other->currency;
}
public function __toString(): string
{
return $this->amount . ' ' . $this->currency;
}
}
DTO(Data Transfer Object)
特徴
- データの転送が目的: ビジネスロジックを含まない
- 単純なデータ構造: ゲッター/セッター以外のメソッドはほとんど持たない
- レイヤー間の境界を越える: 例えば、アプリケーション層からプレゼンテーション層へのデータ転送など
- シリアライズ/デシリアライズ可能: 異なるシステム間でデータを転送できるよう設計する
例:商品情報をAPIに返すためのDTO
class ProductDTO
{
private ?string $id;
private ?string $name;
private ?float $price;
private ?string $currency;
private ?int $stockQuantity;
// コンストラクタ
public function __construct(
?string $id = null,
?string $name = null,
?float $price = null,
?string $currency = null,
?int $stockQuantity = null
) {
$this->id = $id;
$this->name = $name;
$this->price = $price;
$this->currency = $currency;
$this->stockQuantity = $stockQuantity;
}
// エンティティからDTOへの変換メソッド(ファクトリメソッド)
public static function fromEntity(Product $product): self
{
return new self(
$product->getId()->getValue(),
$product->getName(),
$product->getPrice()->getAmount(),
$product->getPrice()->getCurrency(),
$product->getStockQuantity()
);
}
// ゲッター、セッター
public function getId(): ?string
{
return $this->id;
}
public function setId(?string $id): self
{
$this->id = $id;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(?string $name): self
{
$this->name = $name;
return $this;
}
// 長いので省略
// 任意の形に変換する
public function toArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'price' => $this->price,
'currency' => $this->currency,
'stockQuantity' => $this->stockQuantity
];
}
}
使い分け
エンティティの使い所
書いてて(読んでて)しんどいところに、一旦エンティティをかますと使い方が分かりやすいかなと思います。
-
一意のIDで識別する必要がある対象
- ユーザー、注文、商品などのビジネス上の主要概念
- データベースの主要テーブルに対応するオブジェクト
-
状態が変化する対象
- 在庫量が変わる商品
- ステータスが変わる注文(未払い→支払い済み→発送済み)
- プロフィールが更新されるユーザー
-
長いライフサイクルを持つ対象
- 複数のユースケースやセッションにまたがって存在するオブジェクト
- 作成、更新、削除といったライフサイクルイベントがある
-
ビジネスルールが集中する対象
- 「在庫が0未満にならない」などの整合性ルールを持つ
- 「割引適用には条件がある」などのドメインロジックを含む
値オブジェクトの使い所
個人的に一番理解が難しいところですが、エンティティに役割を集中させないために設けるイメージで理解してます。
-
属性値そのものが重要な概念
- 金額と通貨の組み合わせ(Money)
- 住所(Address)
- 日付範囲(DateRange)
- メールアドレス(Email)
-
値の組み合わせに対する操作が必要な場合
- 金額同士の加算、乗算
- 日付範囲の重なり判定
- 緯度経度を使った距離計算
-
不変性が求められる概念
- 一度作成したら変更しない値
- 変更が必要な場合は新しいインスタンスを作る
-
等価性が内容で判断される概念
- 同じ金額と通貨なら同じとみなす
- 同じ郵便番号と住所なら同じとみなす
-
バリデーションのカプセル化
- メールアドレスのフォーマット検証
- 電話番号の形式チェック
DTOの使い所
「データの受け渡しのためだけに使う」ことだけを意識すると、分かりやすいと思いました。
-
レイヤー間のデータ転送
- フロントエンドとバックエンド間(API通信)
- アプリケーション層とドメイン層の間
- インフラ層とドメイン層の間
-
異なる形式へのデータ変換
- エンティティからJSONへの変換
- フォームデータからエンティティへの変換
- レガシーシステムとの連携
-
シリアライズ/デシリアライズが必要なケース
- APIレスポンスとして送信するデータ
- キャッシュに格納するデータ
- メッセージングシステムで送受信するデータ
-
ビューモデルとして
- 画面表示に特化したデータ構造
- 複数のエンティティから必要な情報だけを集めたオブジェクト
-
バッチ処理のデータ構造
- CSVインポート/エクスポートのデータ構造
- レポート出力用データ
比較表
特性 | Entity | Value Object | DTO |
---|---|---|---|
識別子 | あり | なし | 場合による |
可変性 | 可変 | 不変 | 通常は可変 |
ビジネスロジック | あり | あり | なし |
等価性の判断 | IDによる | 値による | 通常は実装しない |
主な用途 | ドメインモデル | ドメインモデル | データ転送 |
ライフサイクル | あり | なし | なし |