Шаблон 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 .
Spring — это не страшно, прослойка из DTO
СОДЕРЖАНИЕ ЦИКЛА СТАТЕЙ Продолжаем говорить про Spring. Сегодня будем разбирать паттерн DTO, для понимания можно почитать тут. Самое сложное в DTO — это понять, зачем оно нужно. Давайте займемся спекуляцией овощей, и заодно, попишем код, может по ходу дела что то и проясниться. Создайте spring-boot проект , подключите h2 и Lombok. Создайте пакеты: entities, repositories, services, utils. В entities создайте сущность Product:
package ru.java.rush.entities; import lombok.Data; import lombok.experimental.Accessors; import org.hibernate.annotations.GenericGenerator; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Accessors(chain = true) @Entity @Data public class ProductEntity < @Id @Column @GenericGenerator(name = "generator", strategy = "increment") @GeneratedValue(generator = "generator") Integer id; @Column String name; @Column Integer purchasePrice;//закупочная цена >
Реализуйте классы ProducRepository, ProducService и класс ItiiateUtil (аналогично прошлой статье). Допустим мы прикупли картофель по оптовой цене 20 рублей за кг., и морковки по 14 рублей за кг. Приобретенные продукты положим в хранилище. Дополним БД записями: [Id =1, name= “Картофель”, purchasePrice = 20] [Id =2, name= “Морковь”, purchasePrice = 14] Как порядочные спекулянты, мы должны выгодно впарить свой товар, для этого давайте красиво упакуем его и накрутим цену. То есть, были у нас грязные и не красивые овощи, наваленные кучей, а станут чистенькие премиум-веган продукты сегмента лакшери. Согласитесь, это будет уже не тот продукт(объект) который мы купили оптом. Для нового продукта создадим пакет dto и в нем класс ProductDto
package ru.java.rush.dto; import lombok.Data; @Data public class ProductDto < Integer id; String name; Integer purchasePrice; String packaging;//упаковка Integer salePrice;//цена реализации >
У ProductDto есть две переменные, которых нет у ProductEntity: «упаковка» и «цена реализации». Объект dto может содержать точно такие же переменные, как и entity, или их может быть больше, или меньше. Мы помним, что конвертация одного объекта в другой – это дело маппинга. В пакете utils создадим класс MappingUtils
package ru.java.rush.utils; import org.springframework.stereotype.Service; import ru.java.rush.dto.ProductDto; import ru.java.rush.entities.ProductEntity; @Service public class MappingUtils < //из entity в dto public ProductDto mapToProductDto(ProductEntity entity)< ProductDto dto = new ProductDto(); dto.setId(entity.getId()); dto.setName(entity.getName()); dto.setPurchasePrice(entity.getPurchasePrice()); return dto; >//из dto в entity public ProductEntity mapToProductEntity(ProductDto dto) < ProductEntity entity = new ProductEntity(); entity.setId(dto.getId()); entity.setName(dto.getName()); entity.setPurchasePrice(dto.getPurchasePrice()); return entity; >>
Просто заполняем поля из одного объекта, аналогичными полями из другого объекта. В классе ProductService реализуем методы для поиска одного продукта или списка продуктов, но перед эти мы конвертируем entity в dto с помощь написанного выше метода.
private final ProductRepository productRepository; private final MappingUtils mappingUtils; //для листа продуктов мы использовали стрим public List findAll() < return productRepository.findAll().stream() //создали из листа стирим .map(mappingUtils::mapToProductDto) //оператором из streamAPI map, использовали для каждого элемента метод mapToProductDto из класса MappingUtils .collect(Collectors.toList()); //превратили стрим обратно в коллекцию, а точнее в лист >//для одиночного продукта обошлись проще public ProductDto findById(Integer id) < return mappingUtils.mapToProductDto( //в метод mapToProductDto productRepository.findById(id) //поместили результат поиска по id .orElse(new ProductEntity()) //если ни чего не нашли, то вернем пустой entity ); >
Что будет если мы сейчас положим эти овощи на витрину? А давайте посмотрим. Для этого в ItiiateUtil напишем следующий код и запустим.
System.out.println("\nВитрина магазина"); for (ProductDto dto : productService.findAll())
На выходе получим: Витрина магазина ProductDto(id=1, name=Картофель, purchasePrice=20, packaging=null, salePrice=null) ProductDto(id=2, name=Морковь, purchasePrice=14, packaging=null, salePrice=null) Ну уж, нет! Такие овощи никто не купит: грязные, не упакованы, да и цена продажи не известна. Настало время бизнес логики. Ее реализуем в классе ProductService. Добавим ка сначала в этот класс пару переменных:
private final Integer margin = 5;//это наша накрутка на цену private final String packaging = "Упаковано в лучшем виде";//так будет выглядеть упаковка
// упаковываем товар public void pack(List list) < list.forEach(productDto ->productDto.setPackaging(packaging) ); > // делаем деньги public void makeMoney(List list) < list.forEach(productDto ->productDto.setSalePrice(productDto.getPurchasePrice() * margin) ); >
List productDtos = productService.findAll(); productService.pack(productDtos); productService.makeMoney(productDtos); System.out.println("\nВитрина магазина"); for (ProductDto dto : productDtos))
Выполняем: Витрина магазина ProductDto(id=1, name=Картофель, purchasePrice=20, packaging=Упаковано в лучшем виде, salePrice=100) ProductDto(id=2, name=Морковь, purchasePrice=14, packaging=Упаковано в лучшем виде, salePrice=70) Товар красиво упакован, есть цена, но вы где-нибудь видели, что бы на витрине писали цену за которую купили оптом и еще id какой-то. Дорабатываем напильником, написанный выше код:
List productDtos = productService.findAll(); productService.pack(productDtos); productService.makeMoney(productDtos); System.out.println("\nВитрина магазина"); for (ProductDto dto : productDtos)
@Service @RequiredArgsConstructor public class InitiateUtils implements CommandLineRunner < private final ProductService productService; @Override public void run(String. args) throws Exception < Listproducts = new ArrayList<>( Arrays.asList( new ProductEntity() .setName("Картофель") .setPurchasePrice(20), new ProductEntity() .setName("Морковь") .setPurchasePrice(14) )); productService.saveAll(products); List productDtos = productService.findAll(); productService.pack(productDtos); productService.makeMoney(productDtos); System.out.println("\nВитрина магазина"); for (ProductDto dto : productDtos) < System.out.println(String.format( "Купите: %s , по цене: %d", dto.getName(), dto.getSalePrice() )); >> >
Запускаем: Витрина магазина Купите: Картофель , по цене: 100 Купите: Морковь , по цене: 70 Другое дело! Теперь по думаем, что dto принесло хорошего, кроме кучи дополнительного кода: 1. Мы можем совершать бизнес-логику не меняя объекты в БД(допустим ну ненужно нам в этой таблице иметь поля про упаковку и цену продажи). Картофель отлично пролежит в хранилище и без упаковки с ценником, они там даже лишние. 2. В этой строчке List productDtos = productService.findAll() мы создали кэш из объектов с которыми удобно работать в рамках бизнес-логики. Это, если бы мы положили часть товара в подсобку магазина. 3. Это нам позволило, совершить два бизнес действия: упаковка и наценка, но запрос в базу сделали только один раз (запросы в базу довольно тяжелы в плане производительности). Товар можно упаковывать, клеить ценник и выкладывать на витрину — постепенно набирая его из подсобки, а не бегать за ним каждый раз в хранилище. На вопрос: «Зачем так сложно?», люди тоже пытаются найти ответ, почитайте. Следующая статья