Lombok — как с ним жить, а не страдать или вариант best practics для lombok

ed07e05c4bf1b1129d11158592976f73

Когда я впервые увидел — lombok, у меня возникло, дикое сопротивление. Было очевидное ощущение, что что-то не так. Я думаю, у многих консервативных разработчиков возникло такое же ощущение. Однако, lombok популярен. Люди его любят, люди его используют. А значит, есть и будут появляться проекты с ним. А значит нам с этим всем придется как-то жить.

провоцирует менять доменную модель откуда вздумается и как вздумается при помощи setter-ов.

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

сгенеренные builder-ы становятся тупыми, а могли бы быть чуть удобней.

он конфликтует с spring data jdbc в плане создания объектов из-за конструкторов.

есть проблемы с иерархией наследования.

откровенно не красивый код с ёлками из аннотаций в начале класса.

есть недочеты в API. например, аннотация Builder на классе, которая автоматом создает конструктор. Вот ни разу не ожидаешь подвоха.

слабая поддержка в плагине (даже на момент сейчас у меня к плагину есть вопросы по навигации, я уже молчу про времена, пару лет назад, когда выходила новая IDEA и плагин ломался).

Сюда можно ещё холивары добавить, но не буду, мое отношение и так понятно.

На своих проектах я постарался выстроить некую систему правил использования lombok.

Аннотация @Setter — запрещена.

Существует антипаттерн — цепочка setter-ов. Для изменения данных в объекте пиши свой метод.Описывай его бизнес смысл в имени метода и в javaDoc. Фиксируй логику метода в тестах.Проверяй состояние сущности в методе. И так далее …

С этой аннотацией и её аналогами цепочки setter-ов на проекте начинают плодиться с устрашающей скоростью. Не успеваешь выбраковывать PullRequest-ы.

Аннотации @Data/@Value — запрещены.

В современной java есть record — его достаточно.

Аннотация @Getter — разрешена только на поле, на классе нельзя.

Это попытка уйти от размазывания логики работы с данными в 100–500 сервисах. Начнем с того, что это не ОПП, когда чужой класс начинает лезть в модель и разбираться в её структуре. Лучше сделать метод, который вернет необходимые данные этому чужому классу-сервису. Этот метод можно покрыть тестами и тем самым улучшить весь проект, который будет адекватно реагировать на снижение качества данных.

Аннотация @Builder — можно на record, нельзя на класс, но можно на конструктор

Выше уже писал, что аннотация @Builder — на классе (если это конечно, не record — там по-другому никак), не самое лучше решение с точки зрения API lombok. Потому что оно порождает конструктор со всеми параметрами не явно, чего не ожидаешь совсем. Однако, возможность вещать @Builder на конструктор — оказывается приятным синтаксическим сахаром. Почему нет.

На модельке с данными (чаще это доменная модель) возможно несколько конструкторов. Во-первых, это конструктор со всеми полями для работы ORM. А так же конструкторы для специальных задач, например, создание по-умолчанию, копирование, копирование из другой сущности и так далее. На все эти конструкторы мы в коде навешиваем аннотацию Builder, но с говорящим названием метода builder-а.

Например, builder для тестов мы назовем testEntity в надежде, что никто в продакшен коде не напишет код вида Task.testEntity().field1(...).field2(...).build()

Проще пояснить на примере.

/**
 * Пример модели-задачи для демонстрации работы с конструторами в условиях lombok
 */
public class Task {
    // .....

    // для того, чтобы Spring Data Jdbc - создал сущность в условиях нескольких конструкторов (для других ORM можно найти аналог)
    @PersistenceConstructor
    // builder для тестов. расчет на то, что в продакшен коде писать Task.testEntity  
    @Builder(builderMethodName = "testEntity", builderClassName = "TaskTestBuilder")
    public Task(UUID id, int version/* ... */) {
        this.id = id;
        this.version = version;
        // ...
    }

    /**
     * Создание задачи
     * @param name Описание задачи
     * @param generator Генератор номеров задач
     */
    @Builder(builderMethodName = "create", builderClassName = "TaskCreateBuilder")
    public Task(String name, NumberGenerator generator) {
        this.id = UUID.randomUUID();
        // ...
    }

    /**
     * Копирующий конструктор
     * @param source Источник данных
     * @param generator Генератор номеров задач
     */
    @Builder(builderMethodName = "copy", builderClassName = "TaskCopyBuilder")
    public Task(Task source, NumberGenerator generator) {
        this.id = UUID.randomUUID();
        this.version = version;
        // ...
    }
}

В указанном примере рассматривается некая модель «задача». У нее есть бизнес смысл. Логика работы. Как видно из примера, эти конструкторы чуть умнее, они не просто принимают поля, но и функции, из которых при необходимости могут достать какие-то специфичные данные. За функциями соответственно скрываются сервисы. Видится, что такой подход более успешен, чем отдельный класс-фабрика, либо метод в сервисе, где вызывается такой же builder, только с чистыми данными. В общем, получается компактно и сильно меньше кода.

И сразу поругаю lombok Builder, прописывать каждый раз builderClassName — напрягает, хотелось бы его каждый раз не писать.

Выключаю префиксы в getter-ах.

На самом деле — это чистая эстетика. Но если в record-ах на получение данных отошли от префикса get, давайте и в других местах делать так же.

# lombok.config
# выключить префиксы у get/set
lombok.accessors.fluent = true

Аккуратно используем @RequiredArgsConstructor

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

@NonNull — выключить NPEа

# lombok.config
# выкидывать осознано NPE кажется не лучшей затеей, чаще ожидаешь какой-то неожиданности в синтаксисе или что-то в этом роде
lombok.nonNull.exceptionType = IllegalArgumentException

Остальные возможности lombok не столь деструктивны при их активном использовании.

Lombok — та ещё задумка. Особенно меня пугает код от начинающих разработчиков, которые ещё не достаточно окрепли в системном мышлении. Будь моя воля, я бы и дальше генерил код в IDEA вместо использования lombok, однако я не один на проекте. Поэтому мы сейчас живем так, как я описал выше, т.е. в некоторой системе правил, которая не позволяет разработчикам сильно запутать проект.

© Habrahabr.ru