Java Records проти звичайних DTO-класів: коли використовувати що?

В сучасних Java-додатках об'єкти передачі даних (DTO) часто використовуються для передачі інформації між рівнями, такими як контролери, сервіси та рівні збереження даних. Традиційно розробники використовували звичайні Java-об’єкти (POJO) з геттерами, сеттерами та конструкторами для представлення DTO. Проте з появою Java Records у Java 14 (у якості попереднього перегляду) і офіційно у Java 16, розробники отримали більш лаконічний і незмінний спосіб визначення DTO.

pic

Але чи варто замінювати всі DTO на записи? Відповідь залежить від конкретного випадку використання.

У цій статті ми розглянемо 5 сценаріїв, коли записи є ідеальним рішенням, та 5 випадків, коли краще залишити традиційні DTO.

Що таке Java Records?

Java Records — це особливий тип класу, призначений для роботи з незмінними носіями даних. Вони усувають зайвий шаблонний код, характерний для традиційних DTO, автоматично генеруючи:

  • Конструктор
  • Геттери (без префіксу “get”)
  • Реалізації equals(), hashCode() та toString()

Приклад Java Record:

public record UserDTO(String name, int age, String email) {}

Це однорядкове визначення еквівалентне наступному громіздкому POJO:

public class UserDTO {  
 private final String name;  
 private final int age;  
 private final String email;  

 public UserDTO(String name, int age, String email) {  
 this.name = name;  
 this.age = age;  
 this.email = email;  
 }  
 public String name() { return name; }  
 public int age() { return age; }  
 public String email() { return email; }  
 @Override  
 public boolean equals(Object o) { /* auto-generated */ }  
 @Override  
 public int hashCode() { /* auto-generated */ }  
 @Override  
 public String toString() { /* auto-generated */ }  
}

Коли використовувати Java Records замість звичайних DTO-класів

Java Records ідеально підходять для ситуацій, коли важливими є незмінність, простота та читабельність. Нижче наведено п'ять ключових сценаріїв, у яких варто розглянути використання записів замість традиційних DTO:

1. Моделі відповіді API

При поверненні даних із RESTful API часто потрібна незмінність для забезпечення потокобезпеки та цілісності даних. Записи забезпечують чистий і ефективний спосіб представлення об'єктів відповіді API.

Приклад:

public record UserResponse(String name, int age, String email) {}

Чому використовувати записи?

  • Зменшує кількість шаблонного коду
  • Забезпечує потокобезпечні та незмінні відповіді

2. Зберігання конфігураційних даних

Якщо потрібно зберігати статичні значення конфігурації програми, які не повинні змінюватися після встановлення, записи стануть відмінним вибором.

Приклад:

public record AppConfig(String appName, int maxUsers) {}

Чому використовувати записи?

  • Незмінність запобігає випадковим змінам
  • Легше керувати фіксованими налаштуваннями програми

3. Представлення пар "ключ-значення" (наприклад, кешування, мапи)

Записи добре підходять для зберігання пар "ключ-значення" у механізмах кешування або операціях відображення, де потрібні прості, лише для читання об'єкти.

Приклад:

public record CacheEntry(String key, Object value) {}

Чому використовувати записи?

  • Спрощує роботу зі зберіганням у мапах
  • Зменшує накладні витрати у часто використовуваних об'єктах

4. Результати запитів до бази даних (проєкції)

Під час роботи з реляційними базами даних іноді потрібна лише частина полів замість усієї сутності. Записи спрощують створення легковагових, незмінних проєкцій.

Приклад:

public record ProductSummary(String name, double price) {}

Чому використовувати записи?

  • Швидше створення об'єктів для інтенсивних запитів на читання
  • Запобігає ненавмисним змінам у результатах запитів

5. Системи, що працюють на подіях (повідомлення)

У архітектурах, заснованих на подіях, повідомлення повинні бути незмінними та легко серіалізуватися.
Записи природним чином відповідають цій вимозі.

Приклад:

public record OrderEvent(String orderId, String status, Instant timestamp) {}

Чому використовувати записи?

  • Забезпечують цілісність подій у розподілених системах
  • Легко серіалізуються та логуються

Коли НЕ варто використовувати Java Records

Попри всі переваги, записи не підходять для всіх сценаріїв. Нижче наведено п’ять ситуацій, у яких краще використовувати звичайні DTO-класи:

1. Потрібні змінні об'єкти

Якщо вам потрібно оновлювати поля об'єкта з часом, записи не підходять, оскільки вони за своєю суттю є незмінними.

Приклад:

class UserProfile {  
 private String name;  
 private int age;  
 public void setName(String name) { this.name = name; }  
 public void setAge(int age) { this.age = age; }  
}

Коли уникати записів?

  • Якщо об’єкти потребують частих оновлень після створення
  • Сценарії, що включають складні робочі процеси

2. Інкапсуляція складної бізнес-логіки

Коли DTO потребують додаткових методів зі складною логікою, правилами перевірки або обчислюваними полями, традиційні класи забезпечують більшу гнучкість.

Приклад:

class CustomerDTO {  
 private String name;  
 private double balance;  
 public double calculateDiscount() { return balance > 1000 ? 10 : 5; }  
}

Коли уникати записів?

  • Необхідно інкапсулювати бізнес-логіку всередині DTO
  • Трансформація даних вимагає відстеження внутрішнього стану

3. Спадковість і ієрархічні структури

Записи не підтримують спадковість, тому якщо ваша програма залежить від поліморфізму, абстрактних класів або розширення функціональності, потрібні традиційні DTO.

Приклад:

abstract class PersonDTO {  
 protected String name;  
 public abstract void display();  
}  

class EmployeeDTO extends PersonDTO {  
 @Override  
 public void display() { System.out.println("Employee: " + name); }  
}

Коли уникати записів?

  • Шаблони проєктування, засновані на спадковості
  • Загальний функціонал, спільний для кількох DTO

4. Вимоги до фреймворків (наприклад, JPA)

Багато фреймворків, як-от Hibernate/JPA, вимагають наявності конструктора без аргументів і змінних полів для збереження сутностей, що робить записи несумісними.

Приклад:

@Entity  
class ProductEntity {  
 @Id  
 @GeneratedValue  
 private Long id;  
 private String name;  
}

Коли уникати записів?

  • Робота з ORM-фреймворками, такими як JPA
  • Якщо фреймворки вимагають проксі-основу для ледачого завантаження

5. Сумісність із бібліотеками, що підтримують JavaBeans

Деякі бібліотеки очікують, що DTO відповідатимуть шаблону JavaBeans з геттерами/сеттерами, які записи не забезпечують.

Приклад:

class OrderDTO {  
 private String orderId;  
 public String getOrderId() { return orderId; }  
 public void setOrderId(String orderId) { this.orderId = orderId; }  
}

Коли уникати записів?

  • Під час використання бібліотек, які покладаються на рефлексію для прив’язки даних
  • Сумісність зі старими фреймворками

Висновок

Java Records — це потужний інструмент, який спрощує створення незмінних носіїв даних, роблячи їх ідеальними для відповідей API, конфігурацій та подійних систем. Проте традиційні DTO-класи залишаються незамінними у випадках, що потребують змінності, спадковості та сумісності з фреймворками.

Використовуйте записи, коли:

  • Вам потрібні незмінність, простота та лаконічний код.

Залишайтеся з традиційними DTO, коли:

  • Ваша програма вимагає змінюваного стану, складної логіки чи підтримки фреймворків.

— — — —

Будь ласка, діліться своїми думками/зворотним зв’язком у коментарях!

Перекладено з: Java Records vs Regular DTO Classes: When to Use What?

Leave a Reply

Your email address will not be published. Required fields are marked *