Data Transfer Object (Объект передачи данных)
Объект, который пересылает данные между процессами для уменьшения количества вызовов методов.
При работе с удалённым интерфейсом, таким как, например, Remote Facade, каждый запрос к нему достаточно затратен. В результате, приходится уменьшать количество вызовов, что означает необходимость передачи большего количества данных за один вызов. Чтобы реализовать это, как вариант, можно использовать множество параметров. Однако, при этом зачастую код получается неуклюжим и неудобным. Также это часто невозможно в таких языках, как Java, которые возвращают лишь одно значение.
Решением здесь является паттерн Data Transfer Object , который может хранить всю необходимую для вызова информацию. Он должен быть сериализуемым для удобной передачи по сети. Обычно используется объект-сборщик для передачи данных между DTO и объектами в приложении.
В сообществе Sun многие используют термин «Value Object» для обозначения этого паттерна. Мартин Фаулер подразумевает под этим термином ( Value Object ) несколько иной паттерн. Обсуждение этого можно прочесть в его книге P of EEA на странице 487.
Использована иллюстрация с сайта Мартина Фаулера.
- Главная
- Список паттернов
- Сайт создан и поддерживается Василием Кулаковым.
Не самые очевидные советы по написанию DTO на Java
Сегодня приложения зачастую имеют распределенный характер. Для подключения к другим сервисам нужно писать больше кода — и при этом стараться сделать его простым.
Чтобы воспользоваться данными из внешней службы, мы обычно преобразуем полезную нагрузку JSON в объект передачи данных (Data Transfer Object, DTO). Код, обрабатывающий DTO, быстро усложняется, но с этим могут помочь несколько советов. Вполне возможно писать DTO, с которыми легче взаимодействовать и которые облегчают написание и чтение кода. Если объединить их вместе — можно упростить себе работу.
Сериализация DTO “по учебнику”
Начнем с типичного способа работы с JSON. Вот структура JSON. Этот JSON представляет пиццу “Реджина”.
<
"name": "Regina",
"ingredients": ["Ham", "Mushrooms", "Mozzarella", "Tomato purée"]
>
Чтобы воспользоваться этими данными у себя в приложении, я создам простой DTO с именем PizzaDto .
import java.util.List;
public static class PizzaDto private String name;
private List ingredients;
public String getName() return name;
>
public void setName(String name) this.name = name;
>
public List getIngredients() return ingredients;
>
public void setIngredients(List ingredients) this.ingredients = ingredients;
>
>
PizzaDto — «старый добрый Java-объект», POJO: объект со свойствами, геттерами, сеттерами и всем остальным. Он отражает структуру JSON, поэтому преобразование между объектом и JSON занимает всего одну строку. Вот пример этого с библиотекой Jackson:
String json = """
<
"name": "Regina",
"ingredients": [ "Ham", "Mushrooms", "Mozzarella", "Tomato purée" ]
>
""";
// из JSON в объект
PizzaDto dto = new ObjectMapper().readValue(json, PizzaDto.class);
// из объекта JSON
json = new ObjectMapper().writeValueAsString(dto);
Преобразование простое и прямолинейное. В чем же тогда проблема?
В реальной жизни DTO бывают довольно сложными. Код для создания и инициализации DTO может включать вплоть до десятков строк. Иногда больше. Это проблема, потому что сложный код содержит больше ошибок и менее чувствителен к изменениям.
Моя первая попытка упростить создание DTO — воспользоваться неизменяемым DTO: таким, который нельзя модифицировать после создания.
Такой подход может показаться странным, если вы не знакомы с этой идеей, поэтому давайте сосредоточимся на ней поподробнее.
Создание неизменяемых DTO
Если говорить просто, то объект неизменяемый, если его состояние не может поменяться после сборки.
Давайте перепишем PizzaDto , чтобы сделать его неизменяемым.
import java.util.List;
public class PizzaDto
private final String name;
private final List ingredients;
public PizzaDto(String name, List ingredients) this.name = name;
if (ingredients != null) ingredients = List.copyOf(ingredients);
>
this.ingredients = ingredients;
>
public String getName() return name;
>
public List getIngredients() return ingredients;
>
>
У неизменяемого объекта нет сеттера. Все его свойства — окончательные и должны быть инициализированы при построении.
Как вы можете видеть, список ингредиентов не хранится как есть. Вместо этого для сохранения неизменяемой копии входных данных используется List.copyOf() . Это не позволяет клиентам изменять ингредиенты, хранящиеся в DTO.
dto
.getIngredients()
.remove("Mushrooms"); // вызывает UnsupportedOperationException
Это важно, потому что пицца “Реджина” без грибов — уже определенно не пицца “Реджина”.
Если серьезнее, то Джошуа Блох, автор книги “Java: эффективное программирование”, дает такую рекомендацию для создания неизменяемых классов:
“Если в вашем классе есть какие-либо поля, которые ссылаются на изменяемые объекты, убедитесь, что клиенты класса не могут получать ссылки на эти объекты”. — Джошуа Блох
Если какое-либо свойство вашего DTO является изменяемым, вам необходимо сделать защитные копии. С их помощью вы предотвратите модификацию вашего DTO извне.
Примечание: начиная с Java 16, существует более краткий способ создания неизменяемых классов через записи.
Хорошо. Теперь у нас есть неизменяемый DTO. Но как это упрощает код?
Преимущества неизменяемости
Неизменяемость приносит много преимуществ, но вот мое любимое: неизменяемые переменные не имеют побочных эффектов.
Рассмотрим на примере. В этом фрагменте кода есть ошибка:
var pizza = make();
verify(pizza);
serve(pizza);
После выполнения этого кода пицца не содержит ожидаемого состояния. Какая строка вызвала проблему?
Попробуем два ответа: сначала с изменяемой переменной, а затем с неизменяемой.
Первый ответ — с изменяемой пиццей. pizza создается с помощью make() , но ее можно изменить в рамках verify() и serve() . Таким образом, к ошибке может приводить любая строка из трех.
Теперь второй ответ — с неизменяемой пиццей. make() возвращает пиццу, но verify() и serve() не могут ее изменить. К проблеме может приводить только make() . Здесь гораздо меньше пространства для расследования. Ошибку легче найти.
С неизменяемыми переменными отладка становится проще. Но это еще не все.
Когда пицца не валидна, метод verify() , вероятно, создает исключение, чтобы прервать процесс. Изменим это. Нам нужно, чтобы метод verify() исправлял невалидные пиццы.
Поскольку pizza — неизменяемый объект, verify() не может просто исправить его. Придется создавать и возвращать измененную пиццу, а клиентский код необходимо адаптировать:
var pizza = make();
pizza = verify(pizza);
serve(pizza);
В этой новой версии очевидно, что метод verify() возвращает новую исправленную пиццу. Неизменяемость делает код более понятным. Его становится легче читать и легче развивать.
Возможно, вы не знаете, но мы и так каждый день пользуемся неизменяемыми объектами. java.lang.String , java.math.BigDecimal , java.io.File — все они неизменяемые.
Есть и другие преимущества В своей книге Джошуа Блох просто рекомендует “минимизировать изменчивость”.
“Неизменяемые классы проще проектировать, реализовывать и использовать, чем изменяемые классы. Они менее подвержены ошибкам и более безопасны”. — Джошуа Блох
Теперь возникает интересный вопрос: можем ли мы поступать так же с DTO?
Неизменяемые DTO… А это осмысленно?
Цель DTO — передача данных между процессами. Объект инициализируется, а затем его состояние не должно меняться. Либо он будет сериализован в JSON, либо будет использоваться клиентом. Это делает неизменность естественной. Неизменяемый DTO будет передавать данные между процессами с гарантией.
Тогда почему я сначала написал изменяемое PizzaDTO , а не неизменяемое? Дело в уверенности, что моей библиотеке JSON требуются геттеры и сеттеры для DTO.
Как оказалось, это не соответствует истине.
Неизменяемые DTO с Jackson
Jackson — самая распространенная JSON-библиотека для Java.
Когда у DTO есть геттеры и сеттеры, Jackson может сопоставить объект с JSON без какой-либо дополнительной настройки. Но с неизменяемыми объектами Jackson нуждается в небольшой помощи. Ему нужно знать, как собирать объект.
Конструктор объекта должен быть снабжен аннотацией @JsonCreator , а каждый аргумент — @JsonProperty . Добавим эти аннотации в конструктор DTO.
// новый импорт:
// import com.fasterxml.jackson.annotation.*;
@JsonCreator
public PizzaDto(
@JsonProperty("name") String name,
@JsonProperty("ingredients") List ingredients) this.name = name;
if (ingredients != null) ingredients = List.copyOf(ingredients);
>
this.ingredients = ingredients;
>
Вот и все. Теперь нас есть неизменяемый DTO, который Jackson может преобразовать в JSON и обратно в объект.
Неизменяемые DTO с Gson и Moshi
Есть две альтернативы Jackson: Gson и Moshi.
С помощью этих библиотек еще проще преобразовать JSON в неизменяемый DTO, потому что им не нужны никакие дополнительные аннотации.
Но почему Jackson вообще требует аннотаций, в отличие от Gson и Moshi?
Никакой магии. Дело в том, что, когда Gson и Moshi генерируют объект из JSON, они создают и инициализируют его путем отражения. Кроме того, они не задействуют конструкторы.
Я не большой поклонник такого подхода. Он вводит в заблуждение, потому что разработчик может вложить некоторую логику в конструктор и никогда не узнать, что он не вызывается. По сравнению с этим, Jackson представляется гораздо более безопасным.
Избегайте нулевых значений
У Jackson есть еще одно преимущество. Если поместить в конструктор некоторую логику, он будет вызываться всегда, независимо от того, создан ли DTO кодом приложения или сгенерирован из JSON.
Можно воспользоваться этим преимуществом для избегания значений null и улучшить конструктор для инициализации полей с ненулевыми значениями.
В приведенном ниже фрагменте кода поля инициализируются пустыми значениями, когда входные данные равны нулю.
// новый импорт :
// import static org.apache.commons.lang3.ObjectUtils.firstNonNull;
@JsonCreator
public PizzaDto(
@JsonProperty("name") String name,
@JsonProperty("ingredients") List ingredients) this.name = firstNonNull(name, ""); // replace null by empty String
this.ingredients = List.copyOf(
firstNonNull(ingredients, List.of()) // replace null by empty List
);
>
В большинстве случаев пустые значения и null не отличаются в поведении. Если заменить нулевые значения пустыми, клиенты смогут пользоваться свойствами DTO без предварительной проверки на null -значения. Кроме того, это снижает шанс появления NullPointerException.
Так вы напишете меньше кода и повысите надежность. Что может быть лучше?
И последнее по счету, но не по важности: создавайте DTO со строителями
Есть еще один совет, как упростить инициализацию DTO. В комплекте с каждым DTO я создаю Builder. Он предоставляет свободный API для облегчения инициализации DTO.
Вот пример создания PizzaDto через сборщик:
var pizza = new PizzaDto.Builder()
.name("Regina")
.ingredients("Mozzarella cheese", "Basil leaves", "Olive oil", "Tomato purée")
.build();
С помощью сложных DTO разработчики делают код более выразительным. Этот шаблон настолько великолепен, что Джошуа Блох почти начинает с него свою книгу “Java: эффективное программирование”.
“Такой клиентский код легко писать и, что более важно, читать”. — Джошуа Блох
Как это работает? Объект builder просто хранит значения, пока мы не вызовем build() , который фактически создает нужный объект с сохраненными значениями.
Вот пример для PizzaDto :
public static final class Builder
private String name;
private List ingredients;
public Builder name(String name) this.name = name;
return this;
>
public Builder ingredients(List ingredients) this.ingredients = ingredients;
return this;
>
/**
* перегружает чтобы тот принимал String varargs
*/
public Builder ingredients(String. ingredients) <
return ingredients(List.of(ingredients));
>
public PizzaDto build() return new PizzaDto(name, ingredients);
>
>
Некоторые пользуются Lombok для создания конструкторов во время компиляции. Это упрощает DTO.
Я предпочитаю генерировать код конструктора с помощью плагина Builder generator IntelliJ. Затем можно добавить перегрузки методов, как в предыдущем фрагменте кода. Конструктор таким образом становится более гибким, а клиентский код — более компактным.
Заключение
Вот основные советы, которые я держу в голове при написании DTO. Соединенные вместе, они действительно улучшат ваш код. Кодовая база становится легче для чтения, проще в обслуживании и, в конечном счете, так проще делиться ею с вашей командой.
- Пишем асинхронный неблокирующий Rest API на Java
- Состояния потоков в Java
- Основы программирования UDP-сокетов на Java
Что такое DTO в Java?
Допустим вы пишите игру шашки. И у вас есть класс Checker. У этого класса будут только поля(цвет, дамка, и т.п.) и геттеры/сеттеры. Вот вам и DTO. Ну и конструктор.
12 дек 2018 в 7:20
Тогда какой смысл в слове Transfer? Он же должен передавать данные по объектам? Как это написано тут DTO
12 дек 2018 в 7:20
@Teemitze DTO переводится как «объект, передающий данные». Данные, которые он передает — это и есть поля.
12 дек 2018 в 7:21
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Объект Customer — DTO.
DTO объект — объект, который не содержит методы. Он может содержать только поля, геттеры/сеттеры, и конструкторы.
Data Transfer Object — объект, передающий данные. Данные — это и есть поля в классе.
Реальный пример — игра шашки. У вас должен быть объект Checker (шашка). У него не должно быть методов, только поля.
public class Checker < private COLOR checkerColor; private Coordinate coordinate; //show checker coordinate private boolean isQueen; //show is the checker queen public Checker(COLOR checkerColor, int xCoordinate, int yCoordinate) < this.checkerColor = checkerColor; coordinate = new Coordinate(xCoordinate, yCoordinate); isQueen = false; >public Checker() <> public COLOR getColor() < return checkerColor; >public Coordinate getCoordinate() < return coordinate; >public boolean isQueen() < return isQueen; >public void setCoordinate(Coordinate coordinate) < this.coordinate.setCoordinates(coordinate.getX(), coordinate.getY()); >public void setQueen() < isQueen = true; >>
Или класс Cell (шашечное поле).
public class Cell < private boolean isBusy; //shows does the field is occupied with the checker private Coordinate coordinate; //show field coordinate public Cell(boolean isBusy, int x, int y) < coordinate = new Coordinate(x, y); this.isBusy = isBusy; >public Cell() <> public boolean isBusy() < return isBusy; >public void setBusy(boolean isBusy) < this.isBusy = isBusy; >public Coordinate getCoordinate() < return coordinate; >>
Или класс Board (доска):
public class Board < private Listcells; //list with 64 Fields() private List checkers; //list with Checkers(), whose number falls from 24 to 0 public Board() < cells = new LinkedList<>(); checkers = new LinkedList<>(); > public List getCells() < return cells; >public List getCheckers() < return checkers; >>
Или класс Coordinate . Хотя у него есть методы(переопределенный equals и compare ), но это методы из Object и он тоже может считаться DTO объектом, т.к. он сделан только для того, что бы хранить данные(координаты).
public class Coordinate < private int x; //x coordinate private int y; //y coordinate public Coordinate(int x, int y) < this.x = x; this.y = y; >public int getX() < return x; >public void setX(int x) < this.x = x; >public int getY() < return y; >public void setY(int y) < this.y = y; >public void setCoordinates(int x, int y) < this.x = x; this.y = y; >@Override public boolean equals(Object o) < if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Coordinate that = (Coordinate) o; return x == that.x && y == that.y; >public boolean compare(Coordinate that, int xMove, int yMove)
Преобразование DTO в сущность — Java: Корпоративные приложения на Spring Boot
Помимо преобразования в DTO, существует и обратная задача — преобразование DTO в Entity. Зачем это делать, если можно наполнять сущность напрямую?
Первая причина — это безопасность. Когда у нас есть API для создания или изменения сущности, обычно мы хотим дать возможность менять только часть свойств. Но если мы используем в @RequestBody нашу сущность напрямую, то у клиента API появляется возможность поменять любые свойства сущности:
@PutMapping("/users/") @ResponseStatus(HttpStatus.OK) // Клиенты могут менять все свойства внутри пользователя public UserDTO update(@RequestBody User user, @PathVariable Long id) repository.save(user); >
Мы не советуем использовать userData как сущность и сразу сохранять в базу — такой подход создает потенциальную опасность.
Вторая причина — схема данных. Со временем именование свойств может меняться и в базе данных, и в API — например, при внедрении новой версии. Разделение сущностей и DTO позволяет делать это независимо. DTO представляет внешний интерфейс для API. В свою очередь, сущности описывают внутреннюю модель данных.
Кроме того, существует еще несколько причин, которые мы разберем подробнее в других уроках:
- Дополнительные преобразования данных перед тем, как они попадут в сущность — например, нормализация электронной почты.
- Дополнительная валидация, которая может понадобиться в конкретном API. Хорошим примером служит подтверждение пароля. Подтверждение пароля не существует на уровне сущности, это вопрос проверки корректности входных данных.
Преобразование из сущности в DTO и наоборот обычно отличаются набором свойств. Например, в большинстве случаев идентификатор генерируется в базе данных — мы не хотим передавать его в API. При этом при возврате ответа в API мы хотим вернуть идентификатор среди остальных свойств. Поэтому есть смысл создавать разные DTO для этих задач.
Разберем пример с созданием и выводом сущности Post :
package io.hexlet.spring.model; import static jakarta.persistence.GenerationType.IDENTITY; import java.time.LocalDate; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import jakarta.persistence.EntityListeners; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.Getter; import lombok.Setter; @Entity @Getter @Setter @Table(name = "posts") @EntityListeners(AuditingEntityListener.class) public class Post @Id @GeneratedValue(strategy = IDENTITY) private Long id; @Column(unique = true) private String slug; private String name; @Column(columnDefinition = "TEXT") private String body; @CreatedDate private LocalDate createdAt; >
Создадим два DTO для каждого действия. Создание потребует три поля — slug , name и body . В вывод добавятся поля id и createdAt :
// Создание поста package io.hexlet.spring.dto; import lombok.Getter; import lombok.Setter; @Setter @Getter public class PostCreateDTO private String slug; private String name; private String body; >
// Вывод поста package io.hexlet.spring.dto; import lombok.Getter; import lombok.Setter; @Setter @Getter public class PostDTO private String id; private String slug; private String name; private String body; private LocalDate createdAt; >
Реализуем создание и вывод. Вывод потребует преобразования только в DTO, а создание — оба преобразования (из сущности в DTO и наоборот):
package io.hexlet.spring.controller.api; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import io.hexlet.spring.dto.PostCreateDTO; import io.hexlet.spring.dto.PostDTO; import io.hexlet.spring.model.Post; import io.hexlet.spring.exception.ResourceNotFoundException; import io.hexlet.spring.repository.PostRepository; @RestController @RequestMapping("/api") public class PostsController @Autowired private PostRepository repository; @GetMapping("/posts/") @ResponseStatus(HttpStatus.OK) public PostDTO show(@PathVariable Long id) var post = repository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Not Found: " + id)); var postDTO = toDTO(post); // Только в DTO return postDTO; > @PostMapping("/posts") @ResponseStatus(HttpStatus.CREATED) public PostDTO create(@RequestBody PostCreateDTO postData) var post = toEntity(postData); // Сначала в Entity repository.save(post); var postDTO = toDTO(post); // Потом в DTO return postDTO; > private PostDTO toDTO(Post post) var dto = new PostDTO(); dto.setId(post.getId()); dto.setSlug(post.getSlug()); dto.setName(post.getName()); dto.setBody(post.getBody()); dto.setCreatedAt(post.getCreatedAt()); return dto; > private Post toEntity(PostCreateDTO postDto) var post = new Post(); post.setSlug(postDto.getSlug()); post.setName(postDto.getName()); post.setBody(postDto.getBody()); return post; > >
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов