Data Transfer Object (Объект передачи данных)
Объект, который пересылает данные между процессами для уменьшения количества вызовов методов.
При работе с удалённым интерфейсом, таким как, например, Remote Facade, каждый запрос к нему достаточно затратен. В результате, приходится уменьшать количество вызовов, что означает необходимость передачи большего количества данных за один вызов. Чтобы реализовать это, как вариант, можно использовать множество параметров. Однако, при этом зачастую код получается неуклюжим и неудобным. Также это часто невозможно в таких языках, как Java, которые возвращают лишь одно значение.
Решением здесь является паттерн Data Transfer Object , который может хранить всю необходимую для вызова информацию. Он должен быть сериализуемым для удобной передачи по сети. Обычно используется объект-сборщик для передачи данных между DTO и объектами в приложении.
В сообществе Sun многие используют термин «Value Object» для обозначения этого паттерна. Мартин Фаулер подразумевает под этим термином ( Value Object ) несколько иной паттерн. Обсуждение этого можно прочесть в его книге P of EEA на странице 487.
Использована иллюстрация с сайта Мартина Фаулера.
- Главная
- Список паттернов
- Сайт создан и поддерживается Василием Кулаковым.
Переосмысление DTO в Java
Привет, Хабр! Представляю вашему вниманию любительский перевод статьи “Rethinking the Java DTO” Стивена Уотермана, где автор рассматривает интересный и нестандартный подход к использованию DTO в Java.
Я провел 12 недель в рамках программы подготовки выпускников Scott Logic, работая с другими выпускниками над внутренним проектом. И был момент, который застопорил меня больше других: структура и стиль написания наших DTO. Это вызывало массу споров и обсуждений на протяжении всего проекта, но в итоге я понял, что мне нравится использовать DTO.
Данный подход не является единственно верным решением, но он довольно интересный и отлично подходит для разработки с использованием современных IDE. Надеюсь, что изначальный шок пройдет и вам он тоже понравится.
Что такое DTO (Data Transfer Object)?
Зачастую, в клиент-серверных приложениях, данные на клиенте (слой представления) и на сервере (слой предметной области) структурируются по-разному. На стороне сервера это дает нам возможность комфортно хранить данные в базе данных или оптимизировать использование данных в угоду производительности, в то же время заниматься “user-friendly” отображением данных на клиенте, и, для серверной части, нужно найти способ как переводить данные из одного формата в другой. Конечно, существуют и другие архитектуры приложений, но мы остановимся на текущей в качестве упрощения. DTO-подобные объекты могут использоваться между любыми двумя слоями представления данных.

DTO — это так называемый value-object на стороне сервера, который хранит данные, используемые в слое представления. Мы разделим DTO на те, что мы используем при запросе (Request) и на те, что мы возвращаем в качестве ответа сервера (Response). В нашем случае, они автоматически сериализуются и десериализуются фреймворком Spring.
Представим, что у нас есть endpoint и DTO для запроса и ответа:
// Getters & Setters, конструкторы, валидация и документация опущены public class CreateProductRequest < private String name; private Double price; >public class ProductResponse < private Long id; private String name; private Double price; >@PostMapping("/products") public ResponseEntity createProduct( @RequestBody CreateProductRequest request ) < /*. */ >
Что делают хорошие DTO?
Во-первых, очень важно понимать, что вы не обязаны использовать DTO. Это прежде всего паттерн и ваш код может работать отлично и без него.
- Если вы используете одно представление данных на оба слоя, вы вполне можете использовать ваши сущности в качестве DTO.
- Если вы хотите вручную заниматься сериализацией ваших сущностей в JSON, то я не могу вас остановить!
Они также помогают документировать слой представления в человеко читаемом виде. Мне нравится использовать DTO и, я думаю, вы тоже могли бы их использовать, ведь это к тому же способствует уменьшению зацепления (decoupling) между слоем представления и предметным слоем, позволяя приложению быть более гибким и уменьшая сложность его дальнейшей разработки.
Тем не менее, не все DTO являются хорошими. Хорошие DTO помогают создавать API согласно лучшим практикам и в соответствии с принципам чистого кода.
Они должны позволять разработчикам писать API, которое внутренне согласовано. Описание параметра на одной из конечных точек (endpoint) должно применяться и к параметрам с тем же именем на всех связанных точках. В качестве примера, возьмём вышепредставленный фрагмент кода. Если поле price при запросе определено как “цена с НДС”, то и в ответе определение поля price не должно измениться. Согласованное API предотвращает ошибки, которые могли возникнуть из-за различий между конечными точками, и в то же время облегчает введение новых разработчиков в проект.
DTO должны быть надёжными и сводить к минимуму необходимость в написании шаблонного кода. Если при написании DTO легко допустить ошибку, то вам нужно прилагать дополнительные усилия, чтобы ваше API оставалось согласованным. DTO должны “легко читаться”, ведь даже если у нас есть хорошее описание данных из слоя представления — оно будет бесполезно, если его тяжело найти.
Давайте посмотрим на примеры DTO, а потом определим, соответствуют ли они нашим требованиям.
Покажи нам код!
Этот код на первый взгляд может показаться довольно странным, но не переживайте. В оставшейся части поста я объясню почему я остановился на данной реализации и какие преимущества она дает. Надеюсь, что вам станет всё понятно и вы оцените данный подход.
Он частично основывается на реальном коде из нашего проекта для выпускников, переведенный в контекст интернет-магазина. В нём каждый продукт имеет название, розничную и оптовую цену. Для хранения цены мы используем тип данных Double, но в реальных проектах вы должны использовать BigDecimal.
public enum ProductDTO private interface Name < @NotBlank String getName(); >private interface Price < @Positive Double getPrice(); >private interface Cost < @Positive Double getCost(); >public enum Request > public enum Response @Value public static class Private implements Id, Name, Price, Cost < Long id; String name; Double price; Double cost; >> >
Мы создаем по одному файлу для каждого контроллера, который содержит базовый enum без значений, в нашем случае это ProductDTO. Внутри него, мы разделяем DTO на те, что относятся к запросам (Request) и на те, что относятся к ответу (Response). На каждый endpoint мы создаем по Request DTO и столько Response DTO сколько нам необходимо. В нашем случае у нас два Response DTO, где Public хранит данные для любого пользователя и Private который дополнительно содержит оптовую цену продукта.
Для каждого параметра мы создаем отдельный интерфейс с таким же именем. Каждый интерфейс содержит один-единственный метод — геттер для параметра, который он определяет. Любая валидация осуществляется через метод интерфейса. В нашем примере, аннотация @NotBlank проверяет что название продукта в DTO не содержит пустую строку.
Для каждого поля который входит в DTO мы реализовываем соответствующий интерфейс. В нашем случае аннотация @Value из библиотеки Lombok делает это за нас, автоматически генерируя геттеры.
Для полного сравнения, с использованием документации, вы можете посмотреть на примеры до и после. Также необходимо понимать, что это небольшие примеры и разница становится более наглядной как только вы начнете добавлять больше DTO.
“Это ужасно!”
Это на самом деле выглядит странно и здесь много необычных моментов. Давайте обсудим несколько из них подробнее.
Три enum и ни один из них не имеет значений! На самом деле мы используем небольшую хитрость для создания namespace-а, т.е. мы можем обращаться к DTO как ProductDTO.Request.Create . Данный “трюк” возможен благодаря тому, что мы ставим ; после каждого enum. Точка с запятой указывает на конец (пустого) списка значений! Использование таких namespace-ов ускоряет поиск нужного DTO, а также можно воспользоваться подсказками в IDE для получения полного списка. Есть и другие возможности добиться того же эффекта, но текущий подход выглядит лаконично, ведь нам не нужно будет использовать конструкции вроде new ProductDTO() и new Create() . Честно говоря, это моё личное предпочтение и вы можете организовать классы как вам угодно.
Мы используем слишком много интерфейсов — по одному на каждый параметр! Мы делаем это потому что считаем данные интерфейсы единственным источником описательной информации относительно параметра который он определяет. Далее мы поговорим об этом чуть больше, но поверьте мне, это принесет свои плоды.
Мы не реализовали методы интерфейсов. Да, выглядит немного странно и я хотел бы найти решение получше. Сейчас мы используем автогенерацию геттеров при помощи Lombok для закрытия контракта и это небольшой хак. Выглядело бы лучше, если бы мы могли объявлять поля сразу в интерфейсе, что позволяло бы создавать DTO в одной строчке кода. Однако, в java нет возможности интерфейсам иметь не статические поля. Если вы будете использовать этот подход в других языках, то возможно ваш код будет более лаконичным.
Это (почти) идеально
Давайте вернемся к нашим требованиям к созданию хорошего DTO. Соотвествует ли им наш подход?
Согласованный синтаксис
Мы определенно улучшили согласованность синтаксиса и это главное почему мы могли бы начать использовать данный паттерн. Каждый API параметр теперь имеет свой синтаксис, определенный через интерфейс. Если DTO содержит опечатку в имени параметра или некорректный тип — код просто не скомпилируется и IDE выдаст вам ошибку. Для примера:
@Value public static class PatchPrice implements Id, Price < String id; // Должен быть тип Long; Double prise; // Опечатка в слове price >
PatchPrice is not abstract and does not override abstract method getId() in Id PatchPrice is not abstract and does not override abstract method getPrice() in Price
К тому же, когда мы используем валидацию на уровне интерфейса, мы исключаем ситуацию, когда один и тот же параметр на одном endpoint проходит валидацию и не проходит её на другом.
Согласованная семантика
Такой стиль написания DTO улучшает понимание кода через наследование документации. Каждый параметр имеет свою семантику которая определена в геттер методах соответствующего ему интерфейса. Пример:
private interface Cost < /** * The amount that it costs us to purchase this product * For the amount we sell a product for, see the parameter. * This data is confidential */ @Positive Double getCost(); >
Как только в DTO мы реализовали данный интерфейс, наша документация автоматически стала доступна через геттер.

Теперь вы гарантировано получаете актуальную и целостную документацию во всех DTO, которые реализовали данный интерфейс. В редких случаях, когда вам нужно добавить API, параметр с уже используемым наименованием и разной семантикой, вам придется создать отдельный интерфейс. Хоть это и неудобно, но заставляет разработчиков задуматься в таких ситуациях, а будущим читателям этого кода понять разницу между схожими параметрами.
Читабельность & Поддерживаемость
Будем честны: в нашем подходе достаточно много шаблонного кода. У нас есть 4 интерфейса, без которых не обойтись, и каждый DTO имеет длинную строку с перечислением интерфейсов. Мы можем вынести интерфейсы в отдельный пакет, что поможет избежать лишних “шумов” в коде c описанием DTO. Но даже после этого, бойлерплейт остается главным недостатком данного подхода, что может оказаться веской причиной для того чтобы использовать другой стиль. Для меня, эти затраты все еще стоят того.
Наш стиль проявляет себя с лучшей стороны, когда нам нужно создать новый DTO. Вы просто пишете @Value public static class [name] implements , перечисляете нужные вам интерфейсы. Далее, добавляете поля пока ваша IDE не перестанет ругаться. Готово! У вас есть DTO с валидацией и документацией.
К тому же, мы видим всю структуру наших DTO классов. Посмотрите на код и вы увидите все что вам нужно знать из сигнатуры класса. Каждое поле указано в списке реализованных интерфейсов. Достаточно нажать ctrl + q в IntelliJ и вы увидите список полей.

В нашем подходе мы пишем валидацию единоразово, т.к. она реализуется через методы интерфейса. Создали новое DTO — получили валидацию в подарок, после реализации интерфейса.
И в заключении, благодаря нашим интерфейсам, мы способны писать переиспользуемые утилитные методы. В качестве примера, рассмотрим ситуацию, когда нам нужно посчитать наценку на товар:
markup = (sale_price - cost_price) / cost_price
В java, мы можем реализовать это используя обобщение:
public static Double getMarkup(T dto)< return (dto.getPrice() - dto.getCost()) / dto.getCost(); >
Входной аргумент имеет тип T , который является обобщением с пересечением типов. dto обязано реализовать оба интерфейса Price и Cost — это означает, что мы не можем использовать данный метод для Public ответа (т.к. он не реализовывает интерфейс Cost ). В стандартном подходе, мы должны были бы добавить метод с двумя аргументами и перегруженные методы для каждого dto (пример). Это переносит работу на вызывающую сторону и добавляет риски возникновения ошибок.
Вывод
Я не жду, что вы сразу же пойдете переписывать все ваши DTO. Но есть несколько деталей которые вы можете почерпнуть для себя:
- Установите единственный источник информации о вашем API параметре.
- Маленькие интерфейсы лучше.
- Попробуйте быть странным, возможно, вам понравится!
P.S. Спасибо, что дочитали до конца мой первый пост на Хабре. Буду рад любой критике относительно перевода, т.к. приходилось немного отходить от оригинала из-за нехватки знаний и опыта.
Зачем и как использовать Объекты передачи данных в Laravel

Data Transfer Object (Объект передачи данных) — паттерн программирования, который, при правильном использовании, может улучшить качество код и удобство его дальнейшего сопровождения. В этой статье мы узнаем, почему нужно использовать DTO и как это сделать в Laravel.
Что такое объект передачи данных?
Объект передачи данных (DTO) используется для передачи данных между уровнями приложения. Это простой объект, который содержит набор данных, обычно в форме свойств или полей, которые представляют определенный объект или концепцию в системе. Основная цель DTO — отделить различные уровни или компоненты, позволяя им взаимодействовать и обмениваться данными без необходимости знать детали реализации друг друга.
Зачем использовать DTO
И для небольших и для сложных систем существуют стандарты и паттерны, которые позволяют создавать код высокого качества, легко поддерживаемый, адаптируемый и обновляемый, и именно здесь в игру вступают DTO.
Допустим, у меня есть такой код:
class UserController extends Controller < public function store(Request $request): JsonResponse < return response()->json([ $this->service->createUser($request->all()), Response::HTTP_CREATED ]); > >
И мне нужно изменить метод createUser . Это, в целом, довольно простой код, но если бы я его создавал год назад, то и не вспомнил бы какие данные возвращает метод $request->all() .
Я мог бы отрефакторить всё это через класс Custom Request :
class UserController extends Controller < public function store(CreateUserRequest $request): JsonResponse < return response()->json([ $this->service->createUser($request->validated()), Response::HTTP_CREATED ]); > >
Но теперь возникают две другие проблемы:
— валидация теперь связана с HTTP-запросом. Если мне нужно вызвать метод createUser , то мне нужно будет снова вручную провалидировать данные.
— данные маппированы, но поскольку $request->validated() возвращает массив, то не получится принудительно указать тип данных, передаваемых методу createUser .
Если мы применим здесь паттерн DTO, то сможем решить обе вышеописанные проблемы. Будем маппировать и валидировать данные в DTO. И теперь, при вызове метода createUser откуда либо, валидация будет произведена автоматически, нужно просто передать DTO в метод.
Как использовать DTO
Как объяснялось выше, DTO могут быть простыми объектами для маппирования свойств. Таким образом, самой простой реализацией DTO было бы создание следующего класса:
class CreateUserDTO < public function __construct( private string $name, private string $email, private string $username, private string $password ) <>// Add getters, setters, validation >
Затем создаём свой DTO из Request :
class UserController extends Controller < public function store(Request $request): JsonResponse < return response()->json([ $this->service->createUser(new CreateUserDTO(. $request->all())), Response::HTTP_CREATED ]); > >
В new CreateUserDTO(. $request->all()) мы используем именованные аргументы из PHP 8 для создания экземпляра CreateUserDTO .
Таким образом, мы решаем проблемы, которые мы подробно описали в предыдущем разделе. Теперь у нас качественный код и его легко поддерживать.
Заключение
В этой статье мы узнали, что такое Объекты передачи данных, почему мы должны использовать их в наших приложениях и как их использовать в Laravel .
Если вы хотите начать использовать DTO в своих приложениях, я создал специальный пакет, который предоставляет базовый DTO-класс с валидацией и преобразованием типов, а также artisan-команду для создания DTO.
Наш Телеграм-канал — следите за новостями о Laravel.
Задать вопросы по урокам можно на нашем форуме.
Шаблон DTO (объект передачи данных)
В этом руководстве мы обсудим шаблон DTO , что это такое, как и когда его использовать. К концу мы будем знать, как правильно его использовать.
2. Узор
DTO или объекты передачи данных — это объекты, которые переносят данные между процессами, чтобы уменьшить количество вызовов методов. Паттерн был впервые представлен Мартином Фаулером в его книге EAA .
Фаулер объяснил, что основная цель шаблона — сократить количество обращений к серверу за счет группирования нескольких параметров в одном вызове. Это снижает нагрузку на сеть при таких удаленных операциях.
Еще одним преимуществом является инкапсуляция логики сериализации (механизм, который переводит структуру объекта и данные в определенный формат, который можно хранить и передавать). Он обеспечивает единую точку изменения нюансов сериализации. Он также отделяет модели предметной области от уровня представления, позволяя им изменяться независимо друг от друга.
3. Как его использовать?
DTO обычно создаются как POJO . Это плоские структуры данных, не содержащие бизнес-логики. Они содержат только хранилище, средства доступа и, в конечном итоге, методы, связанные с сериализацией или синтаксическим анализом.
Данные сопоставляются из моделей предметной области с DTO, как правило, через компонент сопоставления на уровне представления или фасада.
Изображение ниже иллюстрирует взаимодействие между компонентами:
4. Когда его использовать?
DTO пригодятся в системах с удаленными вызовами, так как помогают уменьшить их количество.
DTO также помогают, когда модель предметной области состоит из множества различных объектов, а модели представления нужны все их данные одновременно, или они могут даже сократить круговой обмен между клиентом и сервером.
С помощью DTO мы можем создавать различные представления из наших моделей предметной области , что позволяет нам создавать другие представления той же предметной области, но оптимизировать их в соответствии с потребностями клиентов, не влияя на дизайн нашей предметной области. Такая гибкость является мощным инструментом для решения сложных задач.
5. Вариант использования
Чтобы продемонстрировать реализацию шаблона, мы будем использовать простое приложение с двумя основными моделями предметной области, в данном случае User и Role . Чтобы сосредоточиться на шаблоне, давайте рассмотрим два примера функциональности — поиск пользователей и создание новых пользователей.
5.1. DTO против домена
Ниже приводится определение обеих моделей:
public class User private String id; private String name; private String password; private ListRole> roles; public User(String name, String password, ListRole> roles) this.name = Objects.requireNonNull(name); this.password = this.encrypt(password); this.roles = Objects.requireNonNull(roles); > // Getters and Setters String encrypt(String password) // encryption logic > >
public class Role private String id; private String name; // Constructors, getters and setters >
Теперь давайте посмотрим на DTO, чтобы мы могли сравнить их с моделями предметной области.
На данный момент важно отметить, что DTO представляет собой модель, отправляемую от или к клиенту API.
Следовательно, небольшие различия заключаются либо в том, чтобы упаковать вместе запрос, отправленный на сервер, либо в оптимизации ответа клиента:
public class UserDTO private String name; private ListString> roles; // standard getters and setters >
Вышеупомянутый DTO предоставляет клиенту только необходимую информацию, скрывая пароль, например, из соображений безопасности.
Следующий DTO группирует все данные, необходимые для создания пользователя, и отправляет их на сервер в одном запросе, что оптимизирует взаимодействие с API:
public class UserCreationDTO private String name; private String password; private ListString> roles; // standard getters and setters >
5.2. Подключение обеих сторон
Затем слой, который связывает оба класса, использует компонент сопоставления для передачи данных с одной стороны на другую и наоборот.
Обычно это происходит на уровне представления:
@RestController @RequestMapping("/users") class UserController private UserService userService; private RoleService roleService; private Mapper mapper; // Constructor @GetMapping @ResponseBody public ListUserDTO> getUsers() return userService.getAll() .stream() .map(mapper::toDto) .collect(toList()); > @PostMapping @ResponseBody public UserIdDTO create(@RequestBody UserCreationDTO userDTO) User user = mapper.toUser(userDTO); userDTO.getRoles() .stream() .map(role -> roleService.getOrCreate(role)) .forEach(user::addRole); userService.save(user); return new UserIdDTO(user.getId()); > >
Наконец, у нас есть компонент Mapper , который передает данные, гарантируя, что и DTO, и модель домена не должны знать друг о друге :
@Component class Mapper public UserDTO toDto(User user) String name = user.getName(); ListString> roles = user .getRoles() .stream() .map(Role::getName) .collect(toList()); return new UserDTO(name, roles); > public User toUser(UserCreationDTO userDTO) return new User(userDTO.getName(), userDTO.getPassword(), new ArrayList>()); > >
6. Распространенные ошибки
Хотя шаблон DTO является простым шаблоном проектирования, мы можем допустить несколько ошибок в приложениях, реализующих этот метод.
Первая ошибка — создавать разные DTO для каждого случая. Это увеличит количество классов и картографов, которые нам нужно поддерживать. Постарайтесь сделать их краткими и оцените компромиссы между добавлением одного или повторным использованием существующего.
Мы также хотим избежать попыток использовать один класс для многих сценариев. Эта практика может привести к большим контрактам, в которых многие атрибуты часто не используются.
Еще одна распространенная ошибка — добавлять в эти классы бизнес-логику, чего не должно происходить. Цель шаблона — оптимизировать передачу данных и структуру контрактов. Следовательно, вся бизнес-логика должна находиться на уровне предметной области.
Наконец, у нас есть так называемые LocalDTO , где DTO передают данные между доменами. Проблема еще раз заключается в стоимости обслуживания всего отображения.
Одним из наиболее распространенных аргументов в пользу такого подхода является инкапсуляция модели предметной области. Но проблема здесь в том, чтобы наша модель предметной области сочеталась с моделью постоянства. При их разделении риск раскрытия модели предметной области почти исчезает.
Другие шаблоны достигают аналогичного результата, но обычно они используются в более сложных сценариях, таких как CQRS , Data Mappers , CommandQuerySeparation и т. д.
7. Заключение
В этой статье мы увидели определение паттерна DTO , почему он существует и как его реализовать.
Мы также увидели некоторые распространенные ошибки, связанные с его реализацией, и способы их избежать.
Как обычно, исходный код примера доступен на GitHub .
