README.md

    Небольшой обучающий проект

    Стек:

    • Java 17
    • Spring Boot
    • Spring Data JDBC
    • Test containers & Spring boot test

    Архитектурные требования:

    • Используется DDD
    • В доменной модели запрещено использование Getter-ов и Setter-ов.

    Вопросы по выбранным решениям:

    Почему не JPA?

    hibernate плохо подходит для проектов с DDD архитектурой, я уже молчу о производительности решения.

    Почему мапперы пишутся руками, а не используется orika или mapstruts?

    Во-первых хотел, показать, что мапперы писать не так страшно,

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

    Почему нет lombok-а? Это же пример лучших решений

    На мой скромный взгляд, от lombok больше вреда чем пользы. Он даже хуже, чем автоматические мапперы, т.к.:

    • он откровенно кривое с точки зрения проектирования решение
    • он провоцирует писать бизнесовую логику где придется, получая необходимые данные из getter-ов (lombok1)
    • он провоцирует менять доменную модель откуда вздумается и как вздумается при помощи setter-ов (lombok2)
    • сгенеренные builder-ы становятся тупыми, а могли бы быть чуть удобней
    • он конфликтует с spring data jdbc в плане создания объектов из-за конструкторов
    • ….

    Пункты (lombok1), (lombok2) не совместимы с выбранным DDD подходом. Казалось бы, вот нет Lombok - можно сгенерить get-ы и set-ы руками и пользоваться ими так же. Однако наличие такой библиотеки в стеке именно что провоцирует к такому стилю программирования. При этом, на некоторых моих проектах Lombok есть, я так же с удовольствием пользуюсь некоторыми его фишками, но осмысленно, так где можно это сделать (Честно говоря, очень надоело рубить на ревью неосмысленное использование lombok).
    Ихмо, наличие lombok уже не так важно, т.к. в java есть record-ы и можно писать локаничный код уже стандартными средствами языка.

    Если всё же lombok - давайте порисуем наши варианты использования

    Lombok - взвесить все за и против.

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

    Мои личние претензии к lombok

    Кратко, без воды: 1. провоцирует менять доменную модель откуда вздумается и как вздумается при помощи setter-ов 2. провоцирует писать бизнесовую логику где придется, получая необходимые данные из getter-ов 3. сгенеренные builder-ы становятся тупыми, а могли бы быть чуть удобней 4. он конфликтует с spring data jdbc в плане создания объектов из-за конструкторов 5. есть проблемы с иерархией наследования 6. откровенно не красивый код с ёлками из аннотаций в начале класса 7. есть недочеты в API. например, аннотация Builder на классе, которая автоматом создает конструктор. Вот ни разу не ожидаешь подвоха. 8. слабая поддержка в плагине(даже на момент сейчас у меня к плагину есть вопросы по навигации, я уже молчу про времена, пару лет назад, когда выходила новая IDEA, и плагин ломался).

    Как мы его готовим - best practics
    1. Аннотация @Setter - запрещена. Существует антипаттерн - цепочка setter-ов. Для изменения данных в объекте пиши свой метод. Описывай его бизнес смысл в имени метода и в javaDoc. Фиксируй логику метода в тестах. Проверяй состояние сущности в методе. И так далее …
    2. Аннотации @Data/@Value - запрещены. В java есть record - его достаточно.
    3. Аннотация @Getter - разрешена только на поле, на классе нельзя. Это попытка уйти от размазывания логики работы с данными в 100-500 сервисах. Начнем с того, что это не ОПП, когда чужой класс начинает лезть в модель и разбираться в её структуре. Лучше сделать метод, который вернет необходимые данные этому чужому классу-сервису. Этот метод можно покрыть тестами и тем самым улучшить весь проект, который будет адекватно реагировать на снижение качества данных.
    4. Аннотация @Builder - можно на record, нельзя на класс, но можно на конструктор

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

    На модельке с данными(чаще это доменная модель) возможно несколько конструкторов:

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

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

    тут проще пояснить примером:

    import java.util.UUID;
    
    /**
     * Пример модели-задачи для демонстрации работы с конструторами в условиях 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 - напрягает, хотелось бы его каждый раз не писать.

    1. Выключаю префиксы в getter-ах. На самом деле - это чистая эстетика. Но если в record-ах на получение данных отошли от префикса get, давайте и в других местах делать так же.
    # выключить префиксы у get/set
    lombok.accessors.fluent = true
    
    1. Аккуратно используем @RequiredArgsConstructor Нужно следить за количеством final полей в классе и не допускать слишком большого количества. Раньше, если полей в конструкторе становилось слишком много - это сразу триггер, что делается что-то не так. Данная функциональность с одной стороны убирает необходимость генерить тупой код, с другой стороны уходит важный триггер. Но разработчики ленивы, поэтому - мы используем эту аннотацию.
    2. @NonNull - выключить NPE
    # выкидывать осознано NPE кажется не лучшей затеей, чаще ожидаешь какой-то неожиданности в синтаксисе или что-то в этом роде
    lombok.nonNull.exceptionType = IllegalArgumentException
    

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

    Описание

    Обучающий проект - чтобы показать лучшие практики разработки.

    Конвейеры
    0 успешных
    0 с ошибкой