Строительные блоки DDD: сущности и объекты-значения
Моделируйте предметную область с помощью насыщенных сущностей и неизменяемых объектов-значений
«Строительные блоки DDD: сущности и объекты-значения» — бесплатный урок PHP Academy на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения PHP Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс PHP Academy содержит 4 уроков всего.
Тактическое ядро
Тактические паттерны предметно-ориентированного проектирования дают Вам словарь для моделирования бизнес-понятий в коде. Два фундаментальных строительных блока — это сущности, то есть объекты, определяемые идентичностью во времени, и объекты-значения, определяемые своими атрибутами и рассматриваемые как неизменяемые. Правильное понимание этого различия формирует всё остальное в предметном слое.
Идентичность и значение
Сущность имеет устойчивую идентичность, которая сохраняется даже при изменении её атрибутов: Customer остаётся тем же клиентом после переезда. Объект-значение не имеет идентичности; два объекта-значения с одинаковыми атрибутами взаимозаменяемы, как две купюры по 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
Равенство по значению
Объекты-значения сравниваются по структурному равенству, а не по ссылке. Предоставьте явный метод equals(), вместо того чтобы полагаться на ==, поскольку нестрогое сравнение в PHP имеет неожиданные правила. Объект-значение 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
Неизменяемость и методы изменения через копирование
Поскольку объекты-значения неизменяемы, «изменить» один из них означает вернуть новый экземпляр. Такие методы, как 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;
Сущность
Сущность определяется идентичностью — обычно идентификатором, сгенерированным доменом, а не автоинкрементом базы данных. Равенство определяется идентификаторами. Её атрибуты могут изменяться через методы с насыщенным поведением, которые защищают инварианты.
<?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 — до сохранения, а не ждать этого от базы данных. Благодаря этому можно создать полностью корректную сущность в памяти, ссылаться на неё из разных агрегатов и тестировать код без базы данных. Идентификатор является частью модели, а не артефактом хранения.
Богатое поведение, а не анемичные данные
Распространённый антишаблон — анемичная доменная модель: сущности представляют собой наборы общедоступных методов чтения и записи, тогда как вся логика находится в классах «сервисов». 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. Преимущества:
- Проверка выполняется один раз — при создании.
- Доменные правила, например проверка совпадения валют, находятся рядом с данными.
- Сигнатуры типов документируют назначение и предотвращают путаницу.
О сопоставлении с хранилищем
Объекты-значения часто сопоставляются со встроенными столбцами, а не с отдельными таблицами (в объектно-реляционном отображении, #[Embeddable]). Сущности сопоставляются со строками, ключом которых служит их идентичность. Не помещайте детали хранения в сами доменные объекты: модель не должна знать, что хранится в MySQL. Слой объектно-реляционного отображения или написанный вручную преобразователь переводит данные между богатой доменной моделью и базой данных.
Составные объекты-значения
Объекты-значения можно составлять друг из друга. Address объединяет несколько примитивов в одно целостное понятие со своими правилами равенства и форматирования. Благодаря этому сущность содержит один содержательный тип вместо пяти разрозненных строк, а правила адреса находятся в одном месте.
<?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. Сущности определяются устойчивой идентичностью, содержат богатое поведение и защищают свои инварианты с помощью методов. Объекты-значения неизменяемы, самостоятельно проверяют свою корректность и сравниваются по равенству атрибутов, заменяя одержимость примитивами. Правильное моделирование этих объектов, при котором логика находится внутри них, а не в анемичных наборах данных, служит основой для агрегатов, репозиториев и остального доменного слоя.
Часто задаваемые вопросы
Урок «Строительные блоки DDD: сущности и объекты-значения» бесплатный?
Да — полный текст урока «Строительные блоки DDD: сущности и объекты-значения» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс PHP Academy, подпишись на CoddyKit PRO. Курс PHP Academy содержит 4 уроков всего.
Чему я научусь в уроке «Строительные блоки DDD: сущности и объекты-значения»?
Моделируйте предметную область с помощью насыщенных сущностей и неизменяемых объектов-значений Ты практикуешь PHP Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать PHP Academy?
Предыдущий опыт не требуется. PHP Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.
Сколько времени занимает урок «Строительные блоки DDD: сущности и объекты-значения»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке PHP Academy?
Да. Каждый урок PHP Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Строительные блоки DDD: сущности и объекты-значения
- Агрегаты, репозитории и фабрики
- Доменные события и доменные службы
- Ограниченные контексты и контекстное отображение