PHP: Это DTO или Объект-Значение?
Что такое DTO и как его распознать?
DTO — объект, который содержит примитивные данные (строки, логические значения, числа с плавающей запятой, null, массивы этих типов). Он определяет схемы этих данных, явно объявляя имена полей и их типы. Он может только гарантировать наличие всех этих данных, полагаясь на строгость языка программирования: если конструктор имеет обязательный параметр string
, вы должны передать строку или не сможете создать экземпляр объекта. Однако DTO не даёт никаких гарантий, что значения действительно имеют смысл с точки зрения бизнеса. Строки могут быть пустыми, целые числа могут быть отрицательными и т.д.
Существуют разные дизайны классов для DTO:
/**
* @object-type DTO
*
* Использование конструктора и публичных readonly-свойств
*/
final class AnExample
{
public function __construct(
public readonly string $field,
// ...
) {
}
}
/**
* @object-type DTO
*
* Использование конструктора с приватными readonly-свойствами
* и публичными геттерами
*/
final class AnotherExample
{
public function __construct(
private readonly string $field,
// ...
) {
}
public function field(): string
{
return $this->field;
}
}
Что касается именования DTO: я рекомендую не добавлять DTO
к самому имени. Если вы хотите, что бы было понятно, что это за тип, добавьте комментарий или выдуманную аннотацию (или атрибут), например @object-type
. Это будет полезно для разработчиков не знающих об этих типах объектов. Это может подтолкнуть их к поиску статьи о том, что это значит (может быть, этой статьи :)).
Что такое Объект-Значение и как его распознать?
VO (Объект-Значение) — объект, обернувший одно или несколько значений, или Объектов-Значений. Это гарантирует наличие всех данных, и то, что значения имеют смысл с точки зрения предметной области. Строки больше не будут пустыми, числа будут проверены и соответствовать правильному диапазону. Объект-Значение может это гарантировать, генерируя исключения внутри конструктора, являющегося приватным, заставляя клиента использовать один из статических именованных конструкторов. Это позволяет легко распознать Объект-Значение и чётко отличить его от DTO.
final class AnExample
{
private function __construct(
private string $value
) {
}
public static function fromValue(
string $value
): self {
/*
* Генерирует исключение, когда значение
* не соответствует всем ожиданиям.
*/
return new self($value);
}
}
Пока DTO просто хранит данные для вас и предоставляет чёткую схему для этих данных, Объект-Значение также содержит данные, но предлагает доказательства, что данные соответствуют ожиданиям. Когда класс Объекта-Значение используется в качестве параметра, свойства или типа возвращаемого значения, вы знаете, что имеете дело с правильным значением.
Как мы должны использовать эти типы объектов?
Значение определяется использованием. Если мы неправильно используем DTO
и Объект-Значение
, их имена, в конечном итоге, приобретут другое значение. Возможно, из-за этого и возникает путаница между этими двумя терминами.
DTO
DTO следует использовать только в двух местах: где данные входят в приложение или где они покидают приложение. Несколько примеров использования DTO:
- Когда контроллер получает HTTP POST запрос, данные могут иметь любую форму. Нам нужно перейти от бесформенных данных к данным со схемой (проверенные ключи и типы). Для этого мы можем использовать DTO. Библиотека форм может заполнить этот DTO на основе отправленных данных формы, или мы можем использовать сериализатор для преобразования тела запроса в виде обычного текста в заполненный DTO.
- Когда мы отправляем HTTP POST запрос к веб-сервису, мы можем сначала собрать входные данные в DTO, а затем сериализовать их в тело запроса, которое наш HTTP-клиент может отправить сервису.
- С запросами ситуация аналогичная. Здесь мы можем использовать DTO для представления результата запроса. В качестве примера мы можем передать DTO в шаблон, чтобы отобразить представление на его основе. Мы можем использовать DTO, сериализовать его в JSON и отправить обратно, как ответ API.
- Когда мы отправляем HTTP GET запрос к веб-сервису, мы можем сначала десериализовать ответ API в DTO. Чтобы мы могли применить к нему известную схему, а не просто обращаться к ключам массива и угадывать типы. Клиентские пакеты API обычно предлагают DTO для запросов и ответов.
Объекты-Значение
Объект-Значение используется везде, где мы хотим убедиться, что значение соответствует нашим ожиданиям, и не хотим проверять его снова. Мы также используем его для накопления поведения, связанного с определённым значением. Например, у нас есть Объект-Значений EmailAddress
, мы знаем, что значение было проверено, чтобы выглядеть как валидный адрес электронной почты, поэтому нам не нужно снова проверять его в других местах. Мы также можем добавить методы к объекту, которые извлекают, например, имя пользователя или хоста из адреса электронной почты.
Объекты-Значения часто используются в предметной области, поскольку гарантии или инварианты являются важной частью бизнеса. Но они не могут быть использованы в любом месте приложения, поскольку каждой части приложения требуются способы централизации некоторых правил, предоставления доказательств правильности и накопления связанного поведения.
Вывод
Можно ещё многое сказать об Объектах-Значениях, но цель данной статьи заключалась не в этом (если вы хотите узнать больше, ознакомьтесь с моей книгой Object Design Style Guide
или Implementing Domain-Driven Design
Вона Вернона (Vaughn Vernon)). Цель состояла в том, чтобы максимально наглядно показать разницу между DTO и Объектами-Значениями и, надеюсь, их больше не будут путать. Вот сводная таблица:
DTO:
- Объявляет и применяет схему для данных: имена и типы
- Не даёт гарантии правильности значений
Объект-Значение:
- Оборачивает одно или несколько Значений-Объектов
- Предоставляет доказательства правильности этих значений