Comments 13
Не слишком ли много повторяющегося boilerplate code требует этот подход?

А сколько шаблонного кода требует Java для описания стандартных DTO?


По мне, этот подход решает конкретные (и не надуманые) задачи. Плюс создает некоторое подобие type alias из Haskell (еще один аналог — DOMAIN в SQL), что значительно (субъективно) усиливает систему типов Java.

Не так уж и много если использовать Mapstruct и Lombok. Хотя решение автора с валидацией в интерфейсе довольно элегантное.

Я правильно понимаю, что мне ничего не мешает дописать в Create, Public или Private любое поле без интерфейса, и я это никак не замечу кроме код ревью?

Да, можете дописать что угодно, это обычный статический класс. Тут так же как и с любым паттерном программирования, т.е это скорее «набросок» того как можно решать такие проблемы, нежели готовая «картина».

Чаще всего ваши DTO так или иначе повторяют Domain model. Поэтому по большей части приходится дублировать свойства плюс писать ненужные мэпперы к каждому классу. Мы стараемся сразу определить у domain-класса группы полей, которые потом будут использоваться в DTO при помощи соответствующих интерфейсов:


class User implements BasicInfo, ExtendedInfo, SecurityInfo {
   ...
}
interface BasicInfo {
    String getId();
    String getName();
    String getEmail();
    LocalDate getBirthDate();
}
interface SecurityInfo {
    EnumSet<Role> getRoles();
    LocalDateTime getLastLoginTime();
}

Сериализатору указывается интерфейс DTO, и он пишет только соответствующий сет свойств.
В итоге:


  • исключает дублирование объектов и свойств
  • не нужны мепперы (филдсет сериализуется автоматически)
  • поддержка IDE и все такое
не нужны мепперы (филдсет сериализуется автоматически)

Получается, что котроллер фактически отдаёт наружу доменный объект, я правильно понимаю?

Не совсем. Сериализатор настроен так, чтобы отдавать только сет свойств, указанных в возвращаемом интерфейсе.


@GET
@Path("/users/{userId}")
public BasicInfo getUserBasicInfo(@PathParam("userId") String userId) {
    User user = userDao.findById(userId);
    return user;
}

В этом случае контроллер отдаст JSON:


{
    "id": "12345",
    "name": "Vasya",
    "email" : "vasya@mysite.com",
    "birthDate" : "1970-01-01"
}

Понимаете, какая тут загогулина: фактические userDao.findById(userId) возвращает сущность (если я правильно понял userDao — это реализация JpaRepository).


Теперь представьте, что одним из свойств, входящих во множество, отдаваемое клиенту, является ленивой дочерней сущностью или @Lob. В этом случае мы получим LazyInitException с сообщением "No session", т.к. область действия транзакции прекращается по выходу из findById.


И тут одной из трёх: либо добавлять @Transactional на контроллер, что является грубейшим нарушением принципа разделения слоёв приложения, либо явно загружать ленивую сущность (нужно приседать с графами или явно прописывать fetch), либо возвращать не сущность, а DTO.


Поэтому идея кажется сомнительной, как и внедрение дао/репозиториев в контроллер.

добавлять @Transactional на контроллер, что является грубейшим нарушением принципа разделения слоёв приложения

Принципы у всех разные, и не стоит абсолютизировать никакой из них. Для меня принципиальна бизнес логика приложения. Если она тонет в куче сопроводительного бойлерплейта, то стоит пересмотреть модель использования. Я всегда стараюсь, весь бойлерплейт замести под ковер и автоматизировать по мере возможности.


В данном случае не вижу ничего зазорного, чтобы контроллер был @Transactional. Даже если некоторые properties грузятся как lazy, при сериализации они корректно догрузятся.


либо явно загружать ленивую сущность (нужно приседать с графами или явно прописывать fetch)

Суперинтерфейсы (BasicInfo, ExtendedInfo, SecurityInfo, etc...) как раз по сути определяют наборы свойств, которые необходимо вытащить в каждом конкретном случае, поэтому ничто не мешает модифицировать репозиторий и сделать автоматическую генерацию графа для каждого из суперинтерфейсов.


// dao-метод возвращает список User entity с загруженными свойствами только для суперинтерфейса T
<T super User> List<T> findAll(Class<T> superClass);
...
List<BasicInfo> userInfos = findAll(BasicInfo.class);

Вот вам и автоматизация DTO.

Ну не знаю, как по мне принцип единой ответственности и прочее, как и ТБ писаны кровью, чересчур вольное обращение с ними чревато.


По последнему пункту: если производительность не важна, то проекции созданные из интерфейсов хорошо зайдут. В противном случае будет больно, я писал отдельную статью о причинах этого.


Бенчмарк показывает существенную дороговизну проекций по сравнению с DTO


                                   (count)       Score      Error   Units

findAllByName                            1      16.188 ±    0.643   us/op
findAllByNameUO                          1      13.991 ±    0.208   us/op
findAllByName                          100     235.077 ±    2.407   us/op
findAllByNameUO                        100      65.713 ±    1.618   us/op

findAllByName:·gc.alloc.rate.norm        1   20842.539 ±   24.394    B/op
findAllByNameUO:·gc.alloc.rate.norm      1   13802.823 ±   29.680    B/op
findAllByName:·gc.alloc.rate.norm      100  519894.926 ± 1588.438    B/op
findAllByNameUO:·gc.alloc.rate.norm    100   41812.605 ±   40.003    B/op
Для хранения цены мы используем тип данных Double, но в реальных проектах вы должны использовать BigDecimal.

Вот не надо так, даже в тестовых проектах лучше сразу вырабатывать привычку и писать в боевом режиме.

Может быть шаблон DTO и важен, но, как мне кажется, его слишком превратно понимают, сужая его до отображаемого набора полей или трансформации в более плоские сущности. Предположим, для карточки свойство дают полную сущность и называют DTO как-то вроде UserFullView, а для отображения user`ов в таблице както вроде UserSimplifiedView. Но тут есть нюансы, если вы даете разные dto для разных запросов клиента, то клиенту надо с ними «по-разному работать». Для пример у нас есть класс Контрагент с реквизитами, которые имеют поле ИНН(схематично можно изобразить так):
class Counterparty {
  private Req req {
    prvate String inn;
  }
}

Для карточки свойств уровень вложенности мы не меняем, тогда как для отображения в таблице мне делаем что то вроде того:
class Counterparty {
  prvate String inn;
}


Теперь нам на клиенте, во-первых, надо иметь 2 модели: одну для карточки, другую для таблицы, во-вторых, если мы реализуем поиск по таблице, то мы не сможем его использовать в карточки(под реализуем я подразумеваю, что мы сможем отправить запрос на сервер с фильтрацией по ИНН), потому что ИНН находится на разных уровня и придется чуть иначе формировать запрос. И ради чего? Ради просто наличия DTO как шаблона, который в таком варианте, я вообще сомневаюсь что решает какие-то насущные проблемы.

DTO для меня имеют реальный смысл в двух случаях: если очень сложные модели на бэке и их надо упрощать для клиента; если мы используем DDD, тогда user может быть и сотрудником, и пользователем системы, из-за чего реально одна сущность будет проецироваться в разные поля на клиенте.
Only those users with full accounts are able to leave comments. Log in, please.