0Pricing
PHP Academy · レッスン

DDDの構成要素:エンティティと値オブジェクト

豊かなエンティティと不変の値オブジェクトでドメインをモデル化します。

「DDDの構成要素:エンティティと値オブジェクト」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。

戦術的パターンの中核

ドメイン駆動設計の戦術的パターンは、ビジネス上の概念をコードでモデル化するための語彙を提供します。基礎となる2つの構成要素は、時間の経過における同一性によって定義されるエンティティと、属性によって定義され不変として扱われる値オブジェクトです。この違いを正しく理解することが、ドメイン層の他のすべての設計を形作ります。

同一性と値

エンティティは、属性が変わっても維持される安定した同一性を持ちます。たとえば、Customerは引っ越した後も同じ顧客です。一方、値オブジェクトには同一性がありません。属性が等しい2つの値オブジェクトは、2枚の5ドル札のように互いに置き換え可能です。「すべてのフィールドが変わっても、それは同じものか」と考えてみてください。答えが「はい」なら、それはエンティティです。

値オブジェクト

値オブジェクトは不変で、自己検証可能です。PHP 8.1のreadonlyプロパティによって不変性を強制し、コンストラクターで不変条件を検証することで、無効なインスタンスが存在できないようにします。

<?php
final class Email {
    public function __construct(public readonly string $value) {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("Invalid email: $value");
        }
    }
    public function equals(Email $other): bool {
        return strtolower($this->value) === strtolower($other->value);
    }
}
$a = new Email('Jane@Example.com');
var_dump($a->equals(new Email('jane@example.com'))); // true

値による等価性

値オブジェクトは参照ではなく、構造的等価性によって比較します。PHPの緩い比較には意外なルールがあるため、==に頼るのではなく、明示的なequals()メソッドを提供してください。Money値オブジェクトでは、金額と通貨の両方を考慮する必要があります。

<?php
final class Money {
    public function __construct(
        public readonly int $cents,
        public readonly string $currency
    ) {}
    public function equals(Money $o): bool {
        return $this->cents === $o->cents && $this->currency === $o->currency;
    }
    public function add(Money $o): self {
        if ($this->currency !== $o->currency) {
            throw new DomainException('Currency mismatch');
        }
        return new self($this->cents + $o->cents, $this->currency);
    }
}
$sum = (new Money(500, 'USD'))->add(new Money(250, 'USD'));
echo $sum->cents, PHP_EOL; // 750

不変性と「with」メソッド

値オブジェクトは不変であるため、値を「変更」するとは新しいインスタンスを返すことです。add()やwithAmount()のようなメソッドは決して変更を加えず、新しい値を生成します。これによりエイリアシングによるバグがなくなり、ドメイン全体で値オブジェクトを安心して自由に共有できます。

<?php
final class DateRange {
    public function __construct(
        public readonly DateTimeImmutable $start,
        public readonly DateTimeImmutable $end
    ) {
        if ($end < $start) { throw new InvalidArgumentException('end before start'); }
    }
    public function withEnd(DateTimeImmutable $end): self {
        return new self($this->start, $end);
    }
}
$r = new DateRange(new DateTimeImmutable('2026-01-01'), new DateTimeImmutable('2026-01-10'));
$r2 = $r->withEnd(new DateTimeImmutable('2026-02-01'));
echo $r->end->format('Y-m-d'), ' / ', $r2->end->format('Y-m-d'), PHP_EOL;

エンティティ

エンティティは、通常はデータベースの自動採番ではなく、ドメインで生成されたIDによって定義されます。等価性はIDを比較します。属性は、豊富な振る舞いを持つメソッドを通じて変更でき、それらのメソッドが不変条件を保護します。

<?php
final class Customer {
    private Email $email;
    public function __construct(
        public readonly string $id,
        Email $email
    ) { $this->email = $email; }

    public function changeEmail(Email $new): void { $this->email = $new; }
    public function email(): Email { return $this->email; }
    public function sameIdentityAs(Customer $o): bool { return $this->id === $o->id; }
}
$c = new Customer('cus_1', new Email('a@b.com'));
$c->changeEmail(new Email('c@d.com'));
echo $c->email()->value, PHP_EOL; // c@d.com

同一性の生成

データベースに任せるのではなく、永続化する前にドメイン内で同一性(たとえばUUID)を生成することを優先してください。これにより、メモリ上で完全に有効なエンティティを構築し、集約間で参照し、データベースなしでテストできます。IDはストレージ上の副産物ではなく、モデルの一部です。

貧血したデータではなく豊富な振る舞い

よくあるアンチパターンが貧血ドメインモデルです。エンティティが公開ゲッターやセッターの入れ物になり、すべてのロジックが「サービス」クラスに置かれています。DDDでは振る舞いをエンティティの中に置きます。$order->cancel()は、呼び出し側がステータスフィールドを直接切り替えるのではなく、キャンセルのルールをカプセル化します。

<?php
final class Order {
    private string $status = 'open';
    public function __construct(public readonly string $id) {}
    public function cancel(): void {
        if ($this->status === 'shipped') {
            throw new DomainException('Cannot cancel a shipped order');
        }
        $this->status = 'cancelled';
    }
    public function status(): string { return $this->status; }
}
$o = new Order('o1');
$o->cancel();
echo $o->status(), PHP_EOL; // cancelled

どこでも値オブジェクトを使う

プリミティブ型偏重を値オブジェクトに置き換えてください。string $email、int $cents、string $currencyをそのまま受け渡すのではなく、EmailとMoneyでラップします。メリットは次のとおりです。

  • 構築時に一度だけ検証できます。
  • ドメインルール(通貨の一致)をデータと同じ場所に置けます。
  • 型シグネチャによって意図が明確になり、取り違えを防げます。

永続化マッピングに関する注意

値オブジェクトは、多くの場合、独自のテーブルではなく埋め込みカラム(Doctrineの#[Embeddable])にマッピングします。エンティティは、同一性をキーとする行にマッピングします。永続化に関する関心事はドメインオブジェクト自体から切り離してください。モデルは、自分がMySQLに保存されていることを知るべきではありません。ORMまたは手書きのマッパーが、豊富なドメインモデルとデータベースの間を変換します。

複合値オブジェクト

値オブジェクトは組み合わせて構成できます。Addressは複数のプリミティブ型を、独自の等価性とフォーマットを持つ一貫した1つの概念にまとめます。これにより、エンティティはばらばらの5つの文字列ではなく、1つの豊富な型を保持でき、住所に関するルールも1か所に集約できます。

<?php
final class Address {
    public function __construct(
        public readonly string $street,
        public readonly string $city,
        public readonly string $postcode
    ) {
        if ($postcode === '') { throw new InvalidArgumentException('postcode required'); }
    }
    public function equals(Address $o): bool {
        return $this->street === $o->street
            && $this->city === $o->city
            && $this->postcode === $o->postcode;
    }
    public function oneLine(): string {
        return "{$this->street}, {$this->city} {$this->postcode}";
    }
}
echo (new Address('1 Main St', 'Ankara', '06000'))->oneLine(), PHP_EOL;

理解度チェック

エンティティですか、それとも値オブジェクトですか。

まとめ

DDDの2つの中核的な構成要素を学びました。エンティティは安定した同一性によって定義され、豊富な振る舞いを持ち、メソッドを通じて不変条件を保護します。値オブジェクトは不変で自己検証可能であり、属性の等価性によって比較され、プリミティブ型偏重を解消します。貧血したデータの入れ物ではなくオブジェクトの中にロジックを置き、これらを正しくモデル化することが、集約、リポジトリ、その他のドメイン層の基盤になります。

よくある質問

「DDDの構成要素:エンティティと値オブジェクト」レッスンは無料ですか?

はい。「DDDの構成要素:エンティティと値オブジェクト」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。

「DDDの構成要素:エンティティと値オブジェクト」で何を学びますか?

豊かなエンティティと不変の値オブジェクトでドメインをモデル化します。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

PHP Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。

「DDDの構成要素:エンティティと値オブジェクト」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このPHP Academyレッスンでコードを書いて実行できますか?

はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. DDDの構成要素:エンティティと値オブジェクト
  2. 集約、リポジトリ、ファクトリ
  3. ドメインイベントとドメインサービス
  4. 境界づけられたコンテキストとコンテキストマッピング
← PHP Academyに戻る