Как стать автором
Обновить

Комментарии 29

just so that you know - this library first got released almost 15 years ago. Who are those "conservative developers" then?

Главный момент в Ломбоке, с которым стоит быть осторожнее - equals и hashcode для entity. Тут надо действительно быть аккуратным.

Все остальное в статье абсолютная вкусовщина, которая подана как откровение.

При этом похоже автор не понял, что ломбок - это сокращение генерируемого бойлерплейта (тех же конструкторов, геттеров, сеттеров, тустрингов и прочего). Если у вас другой подход (с иной логикой, которая подразумевает умные объекты, инкапсулирующие свое поведение) - ломбок не про это.

В своей части он прекрасен.

>>equals и hashcode

ну это элементарно, даже не стоит внимания. Хотя я ещё встречаю разрабов, которые не могут назвать контракт equals-hashcode на собесах.

>>ломбок - это сокращение генерируемого бойлерплейта

Только ли? Если убрать механику реализации за скобки, мы получаем новый синтаксический сахар в язык. Именно так воспринимают его многие - как часть языка, которую можно включить и будет типа удобно. Посмотрите-почитайте авторов котлина в той части, где они бьются за то, чтобы не вводить в язык новый синтаксический сахар. Посмотрите их аргументы почему новые фичи вводить не надо. Если смотреть через эту призму, там не всё так прекрасно, как вы говорите.

Вы про толстые ДТОшки? Про нарушение первой S в SOLID? И через какое-то время ещё к нарушению DRY и KISS. Грубо если есть метод почистить пёрышки, то он может обрабатывать все, что в перьях. Не важно это воробей, страус, даже пингвин или даже тираннозавр (есть мнения, что они были с перьями). Но если мы переносим логику почистить пёрышки в объект содержащий поля, в конкретную сущность, то у нас рано или поздно начнутся проблемы, когда понадобится почистить пёрышки другой сущности.

Всю статью не покидало ощущение, что очередная борьба за качество кода. При этом в реальности дублирование логики, повсеместные нарушения SOLID, KISS & DRY. Методы на много - много строк, много тысечестрочные классы. Много такого уже видел. Мы как-то неделю спорили обсуждали куда вставить одно строчный вызов. Все под эгидой борьбы за качество кода. Чтобы было понятнее какой там код. Первый слой контроллеры. Потом сервисы. Вроде не плохо? Нет. Дальше слой протабаф хэндлинга: по сути слой контроллеров. Данные из сервисного слоя заворачивались в протабаф сообщения. Потом слой ещё сервисов. Так вот если бы не борьба - было бы время все это переписать. Но вместо - только боролись за качество. Вызов был воткнут в класс нарушавший все буквы в SOLID . Буквально. В одном месте даже в итерации по списку была проверка и каст к классу.

И по другому быть не может. Это уже из психологии. У меня есть подружка психиатр, нахватался знаний. Борьба за качество - проявление страха ответственности. Форма прокрастинации чтобы не делать, не нести ответственность.

Буду рад если ошибаюсь. Но в последние 6 лет насмотрелся. Потому теперь где не боятся. Где рефакторят когда нужно код. В общем код на голову лучше чем там где борятся (авто исправление вначале исправило борятся на боятся). ?

>> Вы про толстые ДТОшки?

нет, про доменные модели. Комментатор выше утверждает, что если объекты умные, то ломбок тут применять не нужно. по крайней мере вот эту фразу - "Если у вас другой подход (с иной логикой, которая подразумевает умные объекты, инкапсулирующие свое поведение) - ломбок не про это" я понял именно так.

>> Всю статью не покидало ощущение, что очередная борьба за качество кода.

так и есть.

>>Вызов был воткнут в класс нарушавший все буквы в SOLID . Буквально.

Ну и нормально. Есть другая крайность, тупое следование всем принципам. Я тоже иногда осознанно игнорирую что-нибудь из SOLID в угоду простоте, читаемости, качеству. Тут чуйка какая-то должна быть. Наверное, оно вырабатывается с опытом.

Кстати, она не плохо срабатывает во время дебатов внутри команды. По-обсуждаем, а потом получается сильно лучше, чем когда один писал.

>>В общем код на голову лучше чем там где борятся...

Однозначно да. А чтобы не бояться, нужно через TDD писать. Если код покрыт качественно тестами, бояться не чего. Другое дело, писать их тоже надо уметь.

Ой, чую, наприлетает мне минусов.... Начнем с того, что я тоже старпёр не любитель Ломбока, но считаю, что как и всякий инструмент его следует правильно использовать.

О чем в свое время мы договорились в команде:

  1. @Accessors(chain = true) - это удобно. Насчет антипаттерна - мы решили что это некритично, поскольку от человеческой глупости рецепта нет.

  2. С @Entity пользуем только @Accessors(chain = true) @Getter @Setter

  3. Билдеры используем только для иммутабл объектов. Строка вида Test.builder().field1("a").build().setField1("b"); выглядит "по-тупому"

  4. @Data - нужно быть внимательным. Используем только для dto в ресте

  5. @RequiredArgsConstructor - тут (как и автор статьи говорит) нужно быть аккуратным с использованием. Особенно, если у тебя есть однотипные поля.

  6. @NonNull @Slf4j - да, используем.

  7. Остальные аннотации - очень сильно ревьюим и задаемся вопросом: "Зачем? И точно ли от нее есть польза в этой точке?"

1-2 у нас просто другой подход, Entity - это умный объект, со своими методами. Всё работа - поднял объект из базы -> вызвал на нем метод с параметрами -> объект поменял состояние -> сохранил измененный объект. Не допустимо туда выставлять что-то через setter. Этот setter - при ближайшем рассмотрении вырождается в бизнес-метод со своим смыслом и логикой. Всё по классике ООП.

4 - у нас 17-ая java - record лучше )

Всё работа - поднял объект из базы -> вызвал на нем метод с параметрами -> объект поменял состояние -> сохранил измененный объект.

Talk is cheap. Show me the code. Как когда-то сказал один небезысвестный товарищ.

Если мне для перевода этой самой сущности из одного состояния в другое требуется в третьесторонний сервис сходить, я буду сервис в метод сущности передавать? А если таких требуемых сервисов хотя бы штуки 4 их тоже все в аргументами передавать или предлагаете инъекции сервисов прямо в сущность?

Сущность - это такая же DTO, только для базы. А цитируемое мною относится к доменным агрегатам, и если уж зашёл разговор, то одна сущность может отображаться в несколько доменов в зависимости от бизнес-операции.

>>предлагаете инъекции сервисов прямо в сущность?

Такого я не предлагал. Что куда передавать по данным - вопрос к разработчику. Нужно смотреть на решаемую задачу.

А какая парадигма у вас используется domain model или transaction script(если говорить в Фаулеровской терминологии)? Это и будет ответ как вам лучше поступать в том или ином случае.

Что куда передавать по данным - вопрос к разработчику.

Ну, вот я и задаю конкретный вопрос разработчику, пропагандирующему "просто другой подход, Entity - это умный объект, со своими методами". А он отмазывается общими фразами, не давая ответ на поставленный вопрос.

Нужно смотреть на решаемую задачу.

Есть пользователь в базе (UserEntity) для него, например, с госуслуг нужно притащить ФИО. Мне работу по общению с госуслугами прямо в умном UserEntity придётся реализовывать, либо вся "умность" этой сущности будет заключаться в том, чтобы делегировать вызов метода, загружающего ФИО, соответствующему методу сервиса, который будет передан в качестве аргумента, либо как-то по-другому внедрён в сущность.

>>пропагандирующему "просто другой подход, Entity - это умный объект, со своими методами". 

Я его не пропагандирую, я на нем пишу. Года этак с 2010. Для полного погружения в тему советую почитать Фаулера, а этак же что-нибудь на тему DDD (кстати, там в комментариях, люди неплохие ссылки нагнали, т.к. у Эванса - во-первых, очень много воды, во-вторых, слегка устаревшие примеры, так сейчас уже никто не пишет, технологии сильно поменялись, в-третьих, прямо скажу, очень тяжелый язык повествования, я бы эту книгу догонял бы под конец, чтобы добавить мелкий штришки к общей мозаике).

Это немножко не тема данной статьи, но давайте, посмотрим ваш пример. Нужно уточниться. Я так понимаю, сервис синхронный. Предположим, это ещё и сервис высокой доступности.

Примерно код может выглядеть так.

import org.springframework.context.ApplicationEventPublisher;


@Service
@Transactional
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
    private final GosUslugiClient gosUslugiClient;
    private final UserRepository repository;
    private final ApplicationEventPublisher eventPublisher;
    public User checkFio(UUID userId) {
        final var user = repository.findById(userId).orElseThrow(() -> new UserNotFoundException(userId));
        // дергаем сторонний сервис
        // предположим, сервис не доступен, ихмо тут клиент выкидывает 
        // эксепшен, а пользак или кто там снаружи, потом нажмет кнопочку ещё раз
        // если ненайдено - пустой результат   
        final Optional<GosuslugiPersonDto> personDto = gosUslugiClient.findFio(user.snils()); 
        // здесь передаем результат в доменную модель, которая сама 
        // решит, как с ними поступить, и на основе своих действий 
        // может сформировать одно или несколько событий
        
        // есть у меня один проект, где я этот вызов оформил функцией, 
        // чтобы не получить GodObject из цетрального сервиса, т.к. там 
        // очень много интеграций, тогда получаем много сервисов-интеграций, 
        // который инжектят в себя клиент и условный UserService. 
        // Решение с функцией очень понравилось, т.к. ушел от тучи 
        // тупого кода, и всё на своих местах        
        final var event = user.verifyData(personDto);
        repository.save(user);
        // spring-event, чтобы обеспечить слабую связанность между сервисами, 
        // например, метод доменной модели формирует событие, что 
        // данные были обновлены (или не формирует, как повезет)
        
        // мы публикуем это событие, а него могут быть сервисы-подписчики, 
        // которые, например, разошлют почту/смс - о том, о чем нужно. 
        
        // при этом, если мы не уйдем в асинхронность(или в реальные события 
        // - а там и до event driven development - один шаг), 
        
        // всё это будет в одной транзакции и если подписчик по какой-то 
        // причине развалится, то произойдет откат всей операции
        
        // здесь очень один тонкий момент, какие вызовы нужно делать явно, 
        // а какие утаскивать в события:
        //  - перетащишь всё что только можно в сервис, там и до GodObject не далеко
        //  - будешь увлекаться событиями - получишь, тяжелую 
        //     поддержку и непонимание разработчиков что произошло 
        //     и в каком событии(потому что, чтобы такое поддерживать, нужно очень 
        //     глубокое погруженые в бизнес-процессы, а тут через полгода приходишь 
        //     на проект пару багов поправить и сам забыл, что там и где.). 
        
        // А цепочек из 2-х и более событий (когда обработчик события 
        // генерит свои события) вообще лучше не допускать
        
        // для себя выработал подход, в события выносим то, что 
        // напрямую не меняет текущую модель, а является чем-то 
        // второстепенным
        // например, 
        //   -- результат вызова - нужно сохранить в модель, это в событие нельзя. 
        //   -- вызов формирует какой-то запрос - а ответ, приходит асинхронно. 
        //      Такое тоже в событие нельзя (или иногда можно, но очень аккуратно 
        //      с полным пониманием процесса). Высокий риск, что асинхронный ответ 
        //      придет до того, как будет закрыта основная транзакция, получим 
        //      просто конфликт версий при закрытии транзакции, т.к. та же кафка шустрая
        eventPublisher.publishEvent(event);
    } 
}

А теперь усложнимся, и вернемся к самому первому вопросу, на который я побоялся сразу развернуто отвечать, но раз вы проявили заинтересованность, то попробую крупными штрихами. Вы говорили, что дергаете сразу 4 сервиса. Это слишком много, если они все синхронные, то каждый из них под нагрузкой будет подтупливать. Например, в легком режиме, один сервис отвечает 0.5 секунды, но иногда он отвечает 10-15 секунд. Если таких 4, то мы уже получаем 40-60 секунд, а это как минимум слабая отзывчивость синхронного сервиса, как максимум таймауты.

А если вы дернули 3, а 4-й упал? получается, вы первые 3 дернули зря, т.к. просто забыли их данные.

А ещё могут быть интеграции, которые не считывают данные, а запускают какие-то процессы, а тут уже до SAGA рукой подать, а это то ещё усложнение системы в целом. Ихмо, если можно уйти от SAGA, то лучше от него уйти, и подальше, сложностей на проекте и так хватит, зачем ещё усложнять.

ИХМО, в этих случаях стоит пересмотреть общую архитектуру этих интеграций, например, разбивать их на этапы, с сохранением промежуточных результатов, что в свою очередь поменяет вашу статусную модель, и так далее... Кажется, я открыл ящик пандоры, тема очень глубокая...

Благодарю за подробный ответ, но, видимо, мы по-разному понимаем умные сущности. Потому как в вашем примере я ничего "умного" за сущностью не заметил. Вынесение метода, нужного в одном месте конкретного сервиса, на уровень сущности, по-моему, лишнее.

>>Вынесение метода, нужного в одном месте конкретного сервиса, на уровень сущности, по-моему, лишнее.

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

Термин "умные объекты" здесь ввел не я, а один из комментаторов. Если есть умные объекты - должны быть и тупые. Что из них что, до конца не понятно, в какой момент тупой объект умнеет?

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

А что вы понимали под умными объектами?

Чем ваш подход размещения бизнес логики в сервисе отличается от классического transaction script?

Согласен со всем. Но 4 пункт, только если старая Java, без record.

подправил статью. используете @Value, но все почему-то лепят @Data

Любопытно и противоречиво. Например в части getter и setter. У нас множество сервисов-оркестраторов. Часто надо взять данные из одной части, передать в другую, там сформировать дто вызова и это все в JSON и в REST. У меня тут участвуют минимум 3 дто класса с множество полей и одним назначением - передать информацию между разными участками кода. Так почему бы мне не заменить тонну геттеров и сеттеров аннотацией?

Тонну геттеров и сеттеров можно заменить ключевым словом record + builder. Собирать dto через setter - я бы не стал.

Многие проекты на 11 java и ниже, откуда возьмем record?

Просто интересно, почему не упомянули SneakyThrows? Это моя любимая фича. Что думают консерваторы про идею полностью избавиться от checked exceptions? Не холивара ради.

моя тоже. Но пользую не так часто, потому что редко приходится пользоваться API c checked exception. Видимо такие прилетают задачи.

Фанаты Kotlin детектед)

Консерваторы думают, что те, кто применяет SneakyThrows, не понимают смысла механизма исключений, не умеют их применять. В свое время разработчики старались, продумывали систему исключений, чтобы другие разработчики применяли эту систему осознанно. А лентяи просто придумали SneakyThrows и вуаля. Красавчики.

Имхо, это как раз та "фича" из-за которой необходимо запрещать применение ломбока в проекте.

Мне кажется, в новых версиях java он уже и не нужен, для большинства вещей хватает Record-ов.

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

Даже конструкторы, считаю, лишнее. Идея генерирует конструкторы и hash/toString моментально.

полностью согласен. Я запрещаю на своих проектах его совсем. Однако, повторюсь, не все проекты мои, не везде я лидер, статья больше - вариант компромисса с людьми, которым удобно, и они не готовы с ним расставаться.

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

А как же @With на record?

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

Кроме билдеров и ленивых геттеров (которые почему-то не упомянуты автором), все остальное - вред.

Сеттеры и геттеры, не говоря уже о хэш и toString, нужно применять осознанно, не говоря уж о том, что они генерируются идеей (другие иде не знаю) буквально щелчком мыши. Дольше вспоминать, как работает ломбок и проверять по документации.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории