СОДЕРЖАНИЕ ЦИКЛА СТАТЕЙ
Продолжаем говорить про 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<ProductDto> 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()) {
System.out.println(dto);
}
На выходе получим:
Витрина магазина
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<ProductDto> list) {
list.forEach(productDto ->
productDto.setPackaging(packaging)
);
}
// делаем деньги
public void makeMoney(List<ProductDto> list) {
list.forEach(productDto ->
productDto.setSalePrice(productDto.getPurchasePrice() * margin)
);
}
Возвращаемся в ItiiateUtil и выкладывание на витрину заменяем на следующий код
List<ProductDto> productDtos = productService.findAll();
productService.pack(productDtos);
productService.makeMoney(productDtos);
System.out.println("\nВитрина магазина");
for (ProductDto dto : productDtos)) {
System.out.println(dto);
}
Выполняем:
Витрина магазина
ProductDto(id=1, name=Картофель, purchasePrice=20, packaging=Упаковано в лучшем виде, salePrice=100)
ProductDto(id=2, name=Морковь, purchasePrice=14, packaging=Упаковано в лучшем виде, salePrice=70)
Товар красиво упакован, есть цена, но вы где-нибудь видели, что бы на витрине писали цену за которую купили оптом и еще id какой-то.
Дорабатываем напильником, написанный выше код:
List<ProductDto> 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()
));
}
class InitiateUtils в итоге должен быть таким:
@Service
@RequiredArgsConstructor
public class InitiateUtils implements CommandLineRunner {
private final ProductService productService;
@Override
public void run(String... args) throws Exception {
List<ProductEntity> products = new ArrayList<>(
Arrays.asList(
new ProductEntity()
.setName("Картофель")
.setPurchasePrice(20),
new ProductEntity()
.setName("Морковь")
.setPurchasePrice(14)
));
productService.saveAll(products);
List<ProductDto> 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<ProductDto> productDtos = productService.findAll() мы создали кэш из объектов с которыми удобно работать в рамках бизнес-логики. Это, если бы мы положили часть товара в подсобку магазина.
3. Это нам позволило, совершить два бизнес действия: упаковка и наценка, но запрос в базу сделали только один раз (запросы в базу довольно тяжелы в плане производительности). Товар можно упаковывать, клеить ценник и выкладывать на витрину - постепенно набирая его из подсобки, а не бегать за ним каждый раз в хранилище.
На вопрос: «Зачем так сложно?», люди тоже пытаются найти ответ, почитайте.
Следующая статья
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ