Java-Lombok: нужны ли геттеры и сеттеры?

Привет всем энтузиастам Java! Давайте поговорим о Java и рефакторинге. Как известно, Java остается одним из наиболее популярным языком программирования. В каждом новом релизе добавляется все больше фич, с которыми он превращается в идеальное сочетание элементов объектно-ориентированного и функционального языка с введением лямбда-выражений, стримов и CompletableFuture в Java 8.

Всем нам знакомы Java-бины, они же POJO (plain old java objects, “старые-добрые Java-объекты”), они же DTO (data transfer objects, “объекты передачи данных”). Даже несмотря на то, что в обширном Java-сообществе нет согласия по поводу определений, соответствующих каждому из понятий, все они делают примерно то же самое. Названия разные, концепция одна. 

Примечание: в этой статье я буду использовать термин DTO для Java-бинов.

Общие черты DTO:

  1. Все свойства объекта должны быть приватными.
  2. Объект должен содержать конструктор no-args.
  3. Объект должен быть сериализуемым (в большинстве случаев, если только он НЕ предназначен для сетевой передачи).
  4. Объект должен предоставлять способы извлечения (get) и изменения (set) значений свойств с помощью геттеров и сеттеров.

Подводя итог, DTO — это кастомизируемые типы данных, а вышеописанное соглашение — всего лишь условность, которая не является стандартом Java и паттерном проектирования. Впрочем, в конечном счете, паттерн проектирования— это наилучшая практика, доказавшая свою эффективность в решении реальных задач.

Однако существует одна проблема. Что, если DTO содержит, скажем, десять свойств или больше? Вот как выглядит типичный DTO.

Давайте возьмем в качестве примера Employee_DTO, который содержит базовую информацию о сотруднике:

import java.util.Date;

public class Employee_DTO {

    private int employeeId;
    private String name;
    private String email;
    private String department;
    private Employee_DTO manager;
    private Date joiningDate;
    private String designation;
    private String address;
    private int phoneNumber;
    private int emergencyContact;
    private int yearsAtCompany;
    private int totalExperienceInYears;

    public int getEmployeeId() {
        return employeeId;
    }

    public void setEmployeeId(int employeeId) {
        this.employeeId = employeeId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getDepartment() {
        return department;
    }

    public void setDepartment(String department) {
        this.department = department;
    }

    public Employee_DTO getManager() {
        return manager;
    }

    public void setManager(Employee_DTO manager) {
        this.manager = manager;
    }

    public Date getJoiningDate() {
        return joiningDate;
    }

    public void setJoiningDate(Date joiningDate) {
        this.joiningDate = joiningDate;
    }

    public String getDesignation() {
        return designation;
    }

    public void setDesignation(String designation) {
        this.designation = designation;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public int getPhoneNumber() {
        return phoneNumber;
    }

    public void setPhoneNumber(int phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public int getEmergencyContact() {
        return emergencyContact;
    }

    public void setEmergencyContact(int emergencyContact) {
        this.emergencyContact = emergencyContact;
    }

    public int getYearsAtCompany() {
        return yearsAtCompany;
    }

    public void setYearsAtCompany(int yearsAtCompany) {
        this.yearsAtCompany = yearsAtCompany;
    }

    public int getTotalExperienceInYears() {
        return totalExperienceInYears;
    }

    public void setTotalExperienceInYears(int totalExperienceInYears) {
        this.totalExperienceInYears = totalExperienceInYears;
    }

}

Десятки геттеров и сеттеров здесь смотрятся не особенно хорошо. Разумеется, это один из примеров плохо читаемого спагетти-кода. Представьте, что у вас есть десятки или даже сотни таких DTO вместе с геттерами и сеттерами для каждого из них. Для решения этой проблемы есть элегантный способ, позволяющий достичь того же самого результата с менее шаблонным кодом.

Вот тут-то на сцену и выходит Lombok. Он предоставляет простые аннотации и помогает избежать всего того шаблонного кода, который можно себе представить относительно DTO. 

Как Lombok работает за кадром:

Lombok уменьшает шаблонность посредством аннотаций, которые выполняют преобразование классов во время компиляции. Lombok уже поставляется с приличным набором преобразований, но вы также можете создать свои собственные пользовательские преобразования. 

Lombok действует как процессор аннотаций, который играет роль диспетчера, делегирующего функции обработчикам аннотаций Lombok (это то, что мы собираемся создать). Обработчики обнаруживаются с помощью фреймворка под названием SPI. Обработчики аннотаций Lombokобъявляют конкретную аннотацию, которую они обрабатывают. При делегировании полномочий обработчику процессор аннотаций Lombok обеспечивает объект, представляющий абстрактное синтаксическое дерево (AST) аннотируемого узла (например: класс, метод, поле и т. д.). Обработчик аннотаций Lombok может свободно изменять AST, вводя новые узлы: методы, поля и выражения. После этапа обработки аннотаций компилятор будет генерировать байт-код уже из модифицированного AST.

Вкратце, Lombok не генерирует код. Вместо этого он использует неопределенные и недокументированные внутренние вызовы API реализации компилятора для непосредственного изменения абстрактного синтаксического дерева программы в промежутке между чтением исходного кода и выводом скомпилированного байт-кода. 

Отставим теорию. Давайте посмотрим, как быстро получится ввести Lombok в наш проект Java. mavenзависимость выглядит так:

<dependencies>        
        
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
            <scope>provided</scope>
        </dependency>
          
 </dependencies>

И Employee_DTO, проиллюстрированный выше, с Lombok будет выглядеть куда более элегантно и читаемо. Давайте рассмотрим несколько примеров, которые показывают, как можно использовать аннотации Lombok.

  1. Геттеры и сеттеры Lombok для применения ко всему DTO:

Аннотации @Getter и @Setterприменяются ко всему классу, как показано ниже: 

import lombok.Getter;
import lombok.Setter;

import java.util.Date;

@Getter
@Setter
public class Employee_DTO_Lombok {

    private int employeeId;
    private String name;
    private String email;
    private String department;
    private Employee_DTO manager;
    private Date joiningDate;
    private String designation;
    private String address;
    private int phoneNumber;
    private int emergencyContact;
    private int yearsAtCompany;
    private int totalExperienceInYears;

}

2. Геттеры и сеттеры Lombok для применения избирательно в заданном DTO:

В случае если нам нужны геттеры и сеттеры только для определенных переменных/свойств, мы можем добиться и этого тоже:

import lombok.Getter;
import lombok.Setter;

import java.util.Date;

public class Employee_DTO_with_Lombok_Selected {

    @Getter private int employeeId;
    @Getter @Setter private String name;
    private String email;
    private String department;
    private Employee manager;
    @Getter @Setter private Date joiningDate;
    private String designation;
    @Getter private String address;
    private int phoneNumber;
    private int emergencyContact;
    private int yearsAtCompany;
    private int totalExperienceInYears;
}

3. Lombok для замены конструкторов:

а) Замена конструктора all-args

Иногда раскрывать методы-сеттеры не нужно и от клиента требуется устанавливать значения только через вызов конструктора.

Пример Java DTO с конструктором all-args (без использования Lombok):

@Getter
public class Person_DTO {

    Person_DTO(String personName, String personAddress, int phoneNum, String email) {
        this.personName = personName;
        this.personAddress = personAddress;
        this.phoneNum = phoneNum;
        this.email = email;
    }

    private String personName;
    private String personAddress;
    private int phoneNum;
    private String email;
}

После замены конструктора all-args в приведенном выше фрагменте кода аннотацией @AllArgsConstructor он, по существу, сводится к чему-то вроде этого:

import lombok.AllArgsConstructor;

@AllArgsConstructor
public class Person_DTO_AllArgs_Constructor {

    private String personName;
    private String personAddress;
    private int phoneNum;
    private String email;
}

б) Замена конструктора no-args

И c заменой конструктора no-args всё точно так же:

import lombok.NoArgsConstructor;

@NoArgsConstructor
public class Person_DTO_No_Args_Constructor {

    private String personName;
    private String personAddress;
    private int phoneNum;
    private String email;
}

в) Добавление спецификаторов доступа к конструкторам

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

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Person_DTO_NoArgs_Constructor_PrivateAccess {

    private String personName;
    private String personAddress;
    private int phoneNum;
    private String email;
}

4. Lombok для генерации методов toString():

Lombok также генерирует метод toString(), по умолчанию учитывая все свойства DTO и сохраняя их порядок:

@ToString
public class Person_DTO_toString {

    private String personName;
    private String personAddress;
    private int phoneNum;
    private String email;
  
}

Результат выглядит так:

Person_DTO(personName=John Smith, personAddress=4th Avenue-LA, phoneNum=1234567890, [email protected])

5. Lombok для генерации методов equals() и hashCode():

@EqualsAndHashCode генерирует реализации методов equals(Object other) и hashCode(). По умолчанию здесь применяются исключительно нестатические, нестационарные поля, однако их можно изменить (и даже указать, что должны использоваться выходные данные тех или иных методов), пометив элементы типа с помощью @EqualsAndHashCode.Include или @EqualsAndHashCode.Exclude. Другими словами, вы можете указать, какие именно поля или методы хотите использовать, пометив их @EqualsAndHashCode.Include и применив @EqualsAndHashCode(onlyExplicitlyIncluded = true).

import lombok.EqualsAndHashCode;

@EqualsAndHashCode
public class Person_DTO {

    private String personName;
    private String personAddress;
    private int phoneNum;
    private String email;
}

6. @Data: ярлык для @ToString, @EqualsAndHashCode, @Getter по всем полям, @Setter по всем нефинальным полям и @RequiredArgsConstructor

@RequiredArgsConstructor генерирует конструктор с одним параметром для каждого поля, требующего специальной обработки. Все неинициализированные поля, помеченные как final, получают параметр, как и любые поля, помеченные @NonNull, которые не были инициализированы там, где они объявлены.

import lombok.Data;

@Data
public class Person_DTO_Data {

    private String personName;
    private String personAddress;
    private int phoneNum;
    private String email;
}

Это то же самое, что и задействовать все аннотации, перечисленные в приведенном ниже фрагменте кода:

import lombok.*;

@Getter
@Setter
@ToString
@RequiredArgsConstructor
@EqualsAndHashCode
public class Person_DTO_Data_Explained {

    private String personName;
    private String personAddress;
    private int phoneNum;
    private String email;
}

В проекте Lombok доступны и другие аннотации/утилиты, которые очень полезны и удобны.

К примеру,@var, @val, @NonNull, @Cleanup, @Value, @Builder — и это еще не все.

Очень легко рефакторить существующий код/проект и вводить туда Lombok со значительно меньшими усилиями и без поломок. Как уже упоминалось ранее, код становится намного чище, проще и элегантнее.

Добавление плагина Lombok в IDE

Примечание: если вы используете Eclipse или IntelliJ, простое добавление зависимости Lombok в pom.xml не всегда срабатывает, и плагин Lombok необходимо добавлять вручную (в зависимости от вашей IDE).

Добавление плагина Lombok в IntelliJ.

  1. Нажмите Command и клавишу запятой (“⌘” + “,”).
  2. Найдите “Plugin” в верхнем левом углу окна.
  3. Переключите вкладку, то есть перейдите в раздел “Marketplace”.
  4. Найдите “Lombok” и нажмите “Install”.
  5. Перезапустите Intellij.
Добавление плагина Lombok в IntelliJ

Добавление плагина Lombok в Eclypse

  1. Сначала получите jar Lombok. Последняя версия находится на Maven Central.
  2. Запустите jar командой java -jar. Откроется пользовательский интерфейс установщика.
  3. Выбрав элементы установки, нажимаем кнопку Install/Update и выходим из программы установки.
  4. После установки плагина перезапустите Eclipse и убедитесь, что Lombok настроен правильно.

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

Если вы подключены к VPN, попробуйте отключиться от него и после этого добавить плагин.

Если вышеперечисленные приемы не сработают, измените настройки прокси-сервера в IDE и попробуйте добавить плагин еще раз.

Счастливого рефакторинга! 

Читайте также:


Перевод статьи Shreyas N Mahanthappa: Java-Lombok: Do we need getters and setters?