Что такое dto java
DTO vs POCO vs Value Object
Определения DTO, POCO и Value Object
Вначале небольшая ремарка по поводу Value Object. В C# существует похожая концепция, называемая Value Type. Это всего лишь деталь имплементации того, как объекты хранятся в памяти и мы не будем касаться этого. Value Object, о котором пойдет речь, — понятие из среды DDD (Domain-Driven Design).
Ок, давайте начнем. Вы возможно заметили, что такие понятия как DTO, Value Object и POCO часто используются как синонимы. Но действительно ли они означают одно и то же?
DTO — это класс, содержащий данные без какой-либо логики для работы с ними. DTO обычно используются для передачи данных между различными приложениями, либо между слоями внутри одного приложения. Их можно рассматривать как хранилище информации, единственная цель которого — передать эту информацию получателю.
С другой стороны, Value Object — это полноценный член вашей доменной модели. Он подчиняется тем же правилам, что и сущности (Entities). Единственное отличие между Value Object и Entity в том, что у Value Object-а нет собственной идентичности. Это означает, что два Value Object-а с одинаковыми свойствами могут считаться идентичными, в то время как две сущности отличаются друг от друга даже в случае если их свойства полностью совпадают.
Value Object-ы могут содержать логику и обычно они не используются для передачи информации между приложениями.
POJO был представлен Мартином Фаулером в качестве альтернативы для JavaBeans и других «тяжелых» enterprise-конструкций, которые были популярны в ранних 2000-х.
Основной целью POJO было показать, что домен приложения может быть успешно смоделирован без использования JavaBeans. Более того, JavaBeans вообще не должны быть использованы для этой цели.
Другой хороший пример анти-POCO подхода — Entity Framework до версии 4.0. Каждый класс, сгенерированный EF, наследовал от EntityObject, что привносило в домен логику, специфичную для EF. Начиная с версии 4, Entity Framework добавил возможность работать с POCO моделью — возможность использовать классы, которые не наследуются от EntityObject.
Таким образом, понятие POCO означает использование настолько простых классов насколько возможно для моделирования предметной области. Это понятие помогает придерживаться принципов YAGNI, KISS и остальных best practices. POCO классы могут содержать логику.
Корреляция между понятиями
Есть ли связи между этими тремя понятиями? В первую очередь, DTO и Value Object отражают разные концепции и не могут использоваться взаимозаменяемо. С другой стороны, POCO — это надмножество для DTO и Value Object:
Другими словами, Value Object и DTO не наследуют никаким сторонним компонентам и таким образом являются POCO. В то же время, POCO — это более широкое понятие: это может быть Value Object, Entity, DTO или любой другой класс в том случае если он не наследует компонентам, не относящимся напрямую к решаемой вами проблеме.
Вот свойства каждого из них:
Заметьте, что POCO-класс может и иметь, и не иметь собственной идентичности, т.к. он может быть как Value Object, так и Entity. Также, POCO может содержать, а может и не содержать логику внутри себя. Это зависит от того, является ли POCO DTO.
Заключение
Вышесказанное в статье можно суммировать следующим образом:
Переосмысление 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 для запроса и ответа:
Что делают хорошие DTO?
Во-первых, очень важно понимать, что вы не обязаны использовать DTO. Это прежде всего паттерн и ваш код может работать отлично и без него.
Они также помогают документировать слой представления в человеко читаемом виде. Мне нравится использовать DTO и, я думаю, вы тоже могли бы их использовать, ведь это к тому же способствует уменьшению зацепления (decoupling) между слоем представления и предметным слоем, позволяя приложению быть более гибким и уменьшая сложность его дальнейшей разработки.
Тем не менее, не все DTO являются хорошими. Хорошие DTO помогают создавать API согласно лучшим практикам и в соответствии с принципам чистого кода.
Они должны позволять разработчикам писать API, которое внутренне согласовано. Описание параметра на одной из конечных точек (endpoint) должно применяться и к параметрам с тем же именем на всех связанных точках. В качестве примера, возьмём вышепредставленный фрагмент кода. Если поле price при запросе определено как “цена с НДС”, то и в ответе определение поля price не должно измениться. Согласованное API предотвращает ошибки, которые могли возникнуть из-за различий между конечными точками, и в то же время облегчает введение новых разработчиков в проект.
DTO должны быть надёжными и сводить к минимуму необходимость в написании шаблонного кода. Если при написании DTO легко допустить ошибку, то вам нужно прилагать дополнительные усилия, чтобы ваше API оставалось согласованным. DTO должны “легко читаться”, ведь даже если у нас есть хорошее описание данных из слоя представления — оно будет бесполезно, если его тяжело найти.
Давайте посмотрим на примеры DTO, а потом определим, соответствуют ли они нашим требованиям.
Покажи нам код!
Этот код на первый взгляд может показаться довольно странным, но не переживайте. В оставшейся части поста я объясню почему я остановился на данной реализации и какие преимущества она дает. Надеюсь, что вам станет всё понятно и вы оцените данный подход.
Он частично основывается на реальном коде из нашего проекта для выпускников, переведенный в контекст интернет-магазина. В нём каждый продукт имеет название, розничную и оптовую цену. Для хранения цены мы используем тип данных Double, но в реальных проектах вы должны использовать BigDecimal.
Мы создаем по одному файлу для каждого контроллера, который содержит базовый enum без значений, в нашем случае это ProductDTO. Внутри него, мы разделяем DTO на те, что относятся к запросам (Request) и на те, что относятся к ответу (Response). На каждый endpoint мы создаем по Request DTO и столько Response DTO сколько нам необходимо. В нашем случае у нас два Response DTO, где Public хранит данные для любого пользователя и Private который дополнительно содержит оптовую цену продукта.
Для каждого параметра мы создаем отдельный интерфейс с таким же именем. Каждый интерфейс содержит один-единственный метод — геттер для параметра, который он определяет. Любая валидация осуществляется через метод интерфейса. В нашем примере, аннотация @NotBlank проверяет что название продукта в DTO не содержит пустую строку.
Для каждого поля который входит в DTO мы реализовываем соответствующий интерфейс. В нашем случае аннотация @Value из библиотеки Lombok делает это за нас, автоматически генерируя геттеры.
Для полного сравнения, с использованием документации, вы можете посмотреть на примеры до и после. Также необходимо понимать, что это небольшие примеры и разница становится более наглядной как только вы начнете добавлять больше DTO.
“Это ужасно!”
Это на самом деле выглядит странно и здесь много необычных моментов. Давайте обсудим несколько из них подробнее.
Мы используем слишком много интерфейсов — по одному на каждый параметр! Мы делаем это потому что считаем данные интерфейсы единственным источником описательной информации относительно параметра который он определяет. Далее мы поговорим об этом чуть больше, но поверьте мне, это принесет свои плоды.
Мы не реализовали методы интерфейсов. Да, выглядит немного странно и я хотел бы найти решение получше. Сейчас мы используем автогенерацию геттеров при помощи Lombok для закрытия контракта и это небольшой хак. Выглядело бы лучше, если бы мы могли объявлять поля сразу в интерфейсе, что позволяло бы создавать DTO в одной строчке кода. Однако, в java нет возможности интерфейсам иметь не статические поля. Если вы будете использовать этот подход в других языках, то возможно ваш код будет более лаконичным.
Это (почти) идеально
Давайте вернемся к нашим требованиям к созданию хорошего DTO. Соотвествует ли им наш подход?
Согласованный синтаксис
Мы определенно улучшили согласованность синтаксиса и это главное почему мы могли бы начать использовать данный паттерн. Каждый API параметр теперь имеет свой синтаксис, определенный через интерфейс. Если DTO содержит опечатку в имени параметра или некорректный тип — код просто не скомпилируется и IDE выдаст вам ошибку. Для примера:
К тому же, когда мы используем валидацию на уровне интерфейса, мы исключаем ситуацию, когда один и тот же параметр на одном endpoint проходит валидацию и не проходит её на другом.
Согласованная семантика
Такой стиль написания DTO улучшает понимание кода через наследование документации. Каждый параметр имеет свою семантику которая определена в геттер методах соответствующего ему интерфейса. Пример:
Как только в DTO мы реализовали данный интерфейс, наша документация автоматически стала доступна через геттер.
Теперь вы гарантировано получаете актуальную и целостную документацию во всех DTO, которые реализовали данный интерфейс. В редких случаях, когда вам нужно добавить API, параметр с уже используемым наименованием и разной семантикой, вам придется создать отдельный интерфейс. Хоть это и неудобно, но заставляет разработчиков задуматься в таких ситуациях, а будущим читателям этого кода понять разницу между схожими параметрами.
Читабельность & Поддерживаемость
Будем честны: в нашем подходе достаточно много шаблонного кода. У нас есть 4 интерфейса, без которых не обойтись, и каждый DTO имеет длинную строку с перечислением интерфейсов. Мы можем вынести интерфейсы в отдельный пакет, что поможет избежать лишних “шумов” в коде c описанием DTO. Но даже после этого, бойлерплейт остается главным недостатком данного подхода, что может оказаться веской причиной для того чтобы использовать другой стиль. Для меня, эти затраты все еще стоят того.
К тому же, мы видим всю структуру наших DTO классов. Посмотрите на код и вы увидите все что вам нужно знать из сигнатуры класса. Каждое поле указано в списке реализованных интерфейсов. Достаточно нажать ctrl + q в IntelliJ и вы увидите список полей.
В нашем подходе мы пишем валидацию единоразово, т.к. она реализуется через методы интерфейса. Создали новое DTO — получили валидацию в подарок, после реализации интерфейса.
И в заключении, благодаря нашим интерфейсам, мы способны писать переиспользуемые утилитные методы. В качестве примера, рассмотрим ситуацию, когда нам нужно посчитать наценку на товар:
В java, мы можем реализовать это используя обобщение:
Вывод
Я не жду, что вы сразу же пойдете переписывать все ваши DTO. Но есть несколько деталей которые вы можете почерпнуть для себя:
DTO в JS
Информационные системы предназначены для обработки данных, а DTO (Data Transfer Object) является важным концептом в современной разработке. В “классическом” понимании DTO являются простыми объектами (без логики), описывающими структуры данных, передаваемых “по проводам” между разнесенными процессами (remote processes). Зачастую данные «по проводам» передаются в виде JSON.
Если DTO используются для передачи данных между слоями приложения (база данных, бизнес-логика, представления), то, по Фаулеру, это называется LocalDTO. Некоторые разработчики (включая самого Фаулера) негативно относятся к локальным DTO. Основным отрицательным моментом локального применения DTO является необходимость маппинга данных из одной структуры в другую при их передаче от одного слоя приложения к другому.
Тем не менее, DTO являются важным классом объектов в приложениях и в этой статье я покажу JS-код, который на данный момент считаю оптимальным для DTO (в рамках стандартов ECMAScript 2015+).
Структура данных
Во-первых, в коде должна быть отражена сама структура данных. Лучше всего это делать с использованием классов (аннотации JSDoc помогают ориентироваться в типах данных):
Это пример простой структуры данных, где каждый атрибут является примитивом. Если некоторые атрибуты сами являются структурами, то класс выглядит примерно так:
Создание объектов
Как правило, создание экземпляра DTO в половине случаев связано разбором имеющейся структуры данных, полученной «по проводам» с «другой стороны». Поэтому конструктор DTO получает на вход некоторый JS-объект, из которого пытается извлечь знакомые ему данные:
В конструкторе структуры со сложными атрибутами используются конструкторы для соответствующих атрибутов:
Если какой-то атрибут представляет из себя массив, то в конструкторе его разбор выглядит примерно так:
Если какие-то данные должны быть сохранены в атрибуте без разбора, то это тоже возможно (хотя к DTO имеет такое себе отношение):
Метаданные
Например, при выборке данных из БД:
Резюме
Если сводить воедино все три составляющих DTO (структура, конструктор, метаданные), то получается примерно такой es-модуль:
Подобный подход позволяет извлекать знакомые подструктуры данных из больших структур (конфигурационных файлов, ответов от сервисов и т.п.), непосредственно относящиеся к текущему контексту, а IDE, за счёт аннотаций, имеет возможность помогать разработчику ориентироваться в этой подструктуре.
Послесловие
Что привлекло внимание. Во-первых, двухступенчатое создание объекта:
Во-вторых, значения по-умолчанию вешаются на прототип, используемый для создания экземпляров класса:
Это опять-таки направлено на минимизацию потребления памяти при массовом создании экземпляров.
В третьих, отсутствуют метаданные об используемых в DTO атрибутах:
Наверное, с точки зрения разрабов этого генератора кода такая информация показалась им излишней и при необходимости её вполне можно добавить в генератор.
В общем, на мой взгляд, вполне неплохое совпадение изложенного в моей публикации (структура, конструктор, метаданные) с практикой (структура, конструктор). Что касается статических методов, то Safary только 26-го апреля этого года научилась понимать статику (хотя того же эффекта можно добиться за счёт прямого переноса методов в класс: ConfigEmail.initialize = function(obj)<> ).
Коллега @nin-jin в своём комментарии справедливо заметил, что хорошо бы «проверять типы полей перед их сохранением«. Я могу согласиться, что было бы правильным приводить типы данных к ожидаемым (нормализовать данные). Кстати, так и делается в OpenAPI-генераторе:
Насколько я понял, функция Rec предназначена для создания простых дата-объектов (объекты без логики, только с данными) «на лету». В общем-то это и есть DTO, только режима runtime, а не уровня кода (те же метаданные в моём примере позволяют программисту «метить» места использования соотв. DTO и находить их без запуска приложения).
Этот абзац я вставил в начало публикации специально для того, чтобы при сохранении он перенесся в конец текста, а второй абзац стал первым. Вот такая занимательная бага есть сегодня на Хабре.
Какой смысл использовать DTO (объекты передачи данных)?
Какой смысл использовать DTO и является ли это устаревшей концепцией? Я использую POJO в слое представления для передачи и сохранения данных. Могут ли эти POJO рассматриваться как альтернатива DTO?
DTO является шаблоном и не зависит от реализации (POJO / POCO). DTO говорит, что, поскольку каждый вызов любого удаленного интерфейса стоит дорого, ответ на каждый вызов должен принести как можно больше данных. Таким образом, если требуется несколько запросов для доставки данных для конкретной задачи, данные, которые должны быть доставлены, могут быть объединены в DTO, так что только один запрос может принести все необходимые данные. Каталог шаблонов архитектуры корпоративных приложений содержит более подробную информацию.
DTO как концепция (объекты, целью которых является сбор данных, возвращаемых клиенту сервером), безусловно, не устарела.
Однако причина, по которой это понятие устарело, а не просто неправильно, состоит в том, что некоторые (в основном более старые) фреймворки / технологии требуют этого, поскольку их модели предметной области и представления не являются POJOS и вместо этого напрямую связаны с фреймворком.
Хотя DTO не является устаревшим шаблоном, он часто применяется без необходимости, что может сделать его устаревшим.
Например, скажем, у вас есть JSF ManagedBean. Общий вопрос заключается в том, должен ли бин содержать ссылку на сущность JPA напрямую или он должен поддерживать ссылку на некоторый промежуточный объект, который впоследствии преобразуется в сущность. Я слышал, что этот промежуточный объект называется DTO, но если ваши ManagedBeans и сущности работают в одной и той же JVM, то использование шаблона DTO дает мало преимуществ.
Рассмотрим аннотации Bean Validation. Ваши сущности JPA, вероятно, снабжены валидациями @NotNull и @Size. Если вы используете DTO, вам нужно будет повторить эти проверки в DTO, чтобы клиентам, использующим ваш удаленный интерфейс, не нужно было отправлять сообщение, чтобы узнать, что они не прошли базовую проверку. Представьте себе всю эту дополнительную работу по копированию аннотаций Bean Validation между вашим DTO и Entity, но если ваш View и Entities работают в одной и той же JVM, нет необходимости выполнять эту дополнительную работу: просто используйте Entities.
Ссылка IAmTheDude на Каталог шаблонов архитектуры корпоративных приложений предоставляет краткое объяснение DTO, и вот еще несколько ссылок, которые я нашел освещающими:
Генерация DTO и remote интерфейсов из Java в ActionScript
Дано web приложение на Java и Flex. Для связи используется Blaze DS или подобная технология, использующая AMF сериализацию. На стороне сервера и на стороне клиента явно или неявно присутствуют DTO (data transfer objects) и интерфейсы remote сервисов. В подобных приложениях стоит проблема синхронизации кода DTO между клиентом и сервером. Конечно, если приложение полностью покрыто тестами, рассинхронизация между Java и ActionScript исходниками выявится во время тестирования, но есть возможность получить feedback еще раньше – уже во время компиляции.
Для этого нужно генерировать DTO и remote интерфейсы каждый раз при сборке. В случае изменения интерфейсов DTO или remote интерфейсов на сервере они соответствующим образом изменятся и на клиенте. Это позволит узнать о проблемах уже на этапе компиляции, не дожидаясь тестов, кроме того, нельзя исключать вероятности, что тесты не полностью покрывают исходный код.
Для генерации AS DTO есть множество библиотек, например clear toolkit, pimento или Gas3 из Granite DS.
Библиотека Gas3 интегрирована в flexmojos, она позволяет редактировать шаблоны для генерации классов и выглядит более предпочтительно. Далее в примерах используется именно эта библиотека.
Генерация DTO
Генерация remote интерфейса
Теперь сгенерируем remote интерфейс. Создадим модуль git.example.com:jar:jar java класс SampleService и проаннотируем его.
Аннотация @RemoteDestination описывает канал и имя end point’а для сервиса и вешается на класс. @Param задает имя параметра в сгенерированном методе, но она не обязательна. @IgnoredMethod говорит генератору игнорировать помеченный метод.
Для генерации используем те же настройки, что и для DTO. На выходе получаем два класса:
Шаблоны для генерации
Gas3 позволяет модифицировать шаблоны для генерации. Изменим шаблон для генерации remote интерфейсов, чтобы иметь возможность вешать Responder’ы на методы. Шаблоны пишутся на groovy и достаточно объемные, поэтому они не представлены в статье, желающие могут посмотреть шаблоны в репозитории для этого примера тут. Для модификации шаблонов нужно добавить в конфигурацию flexmojos секцию templates:
В результате получаем такой код сервиса:
Кроме remote интерфейсов Gas3 дает возможность менять шаблоны для java интерфейсов, для обычных bean’ов и для JPA entity bean’ов.