DTO — это Data Transfer Object (объект для передачи данных), известный шаблон проектирования.
Проблема
Откуда взялся DTO, зачем он вообще нужен и можно ли без него?
Сразу отвечу. Без него — можно. Но с ним иногда удобнее.
Представим себе обычный метод. Какой-нибудь метод для создания заказа.
public function createOrder()
Что нам минимально потребуется для заказа? Как-то идентифицировать пользователя, а также указать что именно он купил и сколько заплатил.
public function createOrder(string $email, string $goods, int $sum)
Наш бизнес пошёл успешно и у нас добавляются новые параметры:
- Промокод
- Скидка
- Имя менеджера оформившего покупку, для расчёта бонусов от продаж
- Телефон покупателя
- Канал продаж, для отслеживания рекламных кампаний по UTM-меткам
Пожалуй, хватит.
Объявление метода раздулось:
public function createOrder(
string $email,
string $goods,
int $sum,
string $promocode,
int $discount,
string $manager,
string $phone,
string $utmChannel,
)
Выглядит устрашающе. Но это не предел. Параметров может стать гораздо больше, и все они необходимы нам для полноценного описания заказа.
Теперь в каждом месте в коде где используется этот метод, мы получим “лесенку” из параметров. Если продукт большой, то заказ может быть создан не только в одном месте, а во многих. Он может быть создан из консоли, по API, из разных контроллеров с разных пользовательских форм на сайте. В тестах.
При каждом добавлении, изменении, удалении параметра в этом методе мы вынуждены тщательно обновлять все места, где он упоминается. Но с большим количеством параметров очень легко запутаться и допустить ошибку.
Как сократить количество параметров?
Решение
Мы возьмём все параметры и сгруппируем их в один очень простой объект.
class OrderDto
{
public string $email;
public string $goods;
public int $sum;
public string $promocode;
public int $discount;
public string $manager;
public string $phone;
public string $utmChannel;
}
Теперь мы можем передавать в метод этот объект, указав в методе лишь его тип, а не перечисляя все параметры.
public function createOrder(OrderDto $order)
Метод сократился и код стал более читаемым.
Параметры не исчезли и “переехали” в создание объекта, но теперь они явно обозначены:
$order = new OrderDto();
$order->email = 'fox@mulder.com';
$order->goods = 'Alien Blaster';
$order->sum = 5000;
...
Каждому параметру соответствует имя и уже не нужно подглядывать в определение метода, чтобы понять за что отвечает N-й по счёту параметр.
Почему не использовать модель ORM?
Действительно, в модели ORM как правило перечислены все те же самые поля.
Почему бы просто не создавать такую модель и не передавать её везде вместо того чтобы делать отдельный класс DTO?
Резонный вопрос.
Дело в том, что в этом случае знания о деталях общения с БД проникают во весь остальной код, который с БД напрямую не работает.
Из-за этого, понадобится нам переименовать колонку в таблице — и мы вынуждены править модель везде, включая контроллеры.
Будет затронуто больше кода при изменениях, а следовательно увеличивается вероятность сделать ошибку.
Программисты которые много раз обжигались на том, что код приложения очень тесно связан со структурой БД, стараются этого не допускать и отделять БД от остального кода, что как раз и можно реализовать с помощью DTO.
Что не является DTO?
По определению, DTO это “просто данные”, то есть голая структура данных.
-
Модель ORM не может являться DTO так как содержит как минимум связь с БД, а иногда и логику валидации и фильтрации и прочую.
-
Часто путают шаблоны проектирования DTO и Value Object.
Некоторые разработчики даже утвеждают, что разницы вообще нет и Value Object является DTO.
Но DTO в отличие от Value Object не должен содержать логику. Именно отсутствие логики и делает DTO таким удобным универсальным средством для передачи любых структурированных данных.
Нам достаточно взглянуть на список полей, чтобы понять что содержится в DTO, конечно, если поля названы как следует.
Пока мы соблюдаем эту простоту — поля без логики — наш объект будет полноценным DTO.
Плюсы
Код с DTO становится более строгим и чистым, сокращается количество параметров в методах.
Но это не всё.
Также мы можем использовать DTO:
-
Для расчёта скидки в отдельном сервисе
$this->fillDiscount($order); // Посчитали скидку, записали в поле "discount" ... $this->createOrder($order);
-
Для того чтобы вынести всю фильтрацию и валидацию запроса в отдельное место и “получить” чистый объект
$order = $this->readOrder($request); // Все поля заполнили из запроса ... $this->createOrder($order);
-
Для того чтобы описать структуры внешнего API: того API к которому подключается наше приложение