Pull to refresh
20
0
Семен Киреков @kirekov

Java Team Lead, спикер, лектор/автор в МТС Тета

Send message

Вообще, многие проблемы в статье решаются использованием языка со статической, а не динамической типизацией. Взял ту же Java/C#/Scala/Rust/etc и забыл о проблемах «я не правильно указал символ в django-конфиге».

Примеры с JavaScript валидные, но надуманные. Если брать реакт, то вряд ли кто-то будет писать проект с нуля на JavaScript, а не typescript.

Меня всегда интересовало, зачем вообще использовать динамические языки для чего-то сложнее, чем скрипт/или dag в airflow? Берешь статическую типизацию и больше не надо есть кактус

Интересная статья. Спасибо за материал. Но все-таки пример здесь довольно простой. Я думаю, что конкретно в данном контексте лучший вариант был бы иным:

  1. Объявить конструктор в сущности как package-private (так это называется в Java, возможно, в C# по-другому)

  2. Добавить рядом доменный сервис UserCreateService

  3. В нем расположить логику по проверки уникального email-а при создании пользователя

Таким образом, приложение может использовать лишь UserCreateService, не имея доступа к созданию User напрямую. Тогда и можно гарантировать, что Use Case всегда будет соблюдаться.

Все-таки доменное событие обычно используется, чтобы сообщить внешнему миру, что в агрегате произошли какие-то изменения. То есть теоретически у события может быть несколько обработчиков. А судя по описанному кейсу, предполагается, что обработчик всегда один и тот же.
Просто мы тоже сначала именно так проверяли уникальность названий. Но в конце концов, в этом стало тяжело разбираться.

P.S. А вообще, эта проблема возникает только в тех ситуациях, когда User – корень агрегата. Если предположить, что был бы UserGroup, в рамках которой email-ы должны быть уникальны, то тогда достаточно было бы предоставить методы UserGroup.createUser(...), внутри которого и проверялся бы email

Спасибо за фидбек.

  1. Целесообразность кэширования нужно определять по результатам load testing. Если система и так справляется, то не вижу в этом смысла, так как кэширование неизбежно усложняет код и может добавить баги в плане консистентности данных. А последнее для ролей крайне важно. Например, вы забрали роль у человека, а в кэше она осталась. То есть он продолжает получать доступ, как и раньше. Вообще, как я вижу, гораздо лучше разбираться в причинах, почему запрос на выборку ролей тормозит. А их может быть много: проблема n + 1, отсутствие нужных индексов, лишние запросы, которые можно устранить, и так далее.

  2. Не применял Spring ACL, так что здесь не могу что-то посоветовать. Но исходя из инфы, которую нашел на Baeldung, как я понимаю, Spring ACL также следует концепции плоской модели (если я ошибаюсь, поправьте меня). А в таком случае проблемы будут все те же самые, как и при классическом вызове метода Authentication.getAuthorities().

  1. Такое действительно может произойти. Обычно проблема не в том, что задачи описаны двусмысленно. Это следствие. Проблема в коммуникации. Люди решают, что нужно сделать голосом, а потом заводят задачу для соблюдения формальности. Здесь нужно сместить фокус таким образом, чтобы проблемы и вопросы по задаче решались в рамках заведенного тикета. Проще говоря, если задача описана не понятно, я оставляю комментарий к ней до выяснения обстоятельств. Как все вопросы разрешатся, можно поправить описание и только после приступать к выполнению.

  2. Вообще, если честно, у нас такой проблемы не возникало. Для срочного решения можно ставить к таким задачам какой-нибудь лейбл wrong_task. На CI/CD проверять его наличие или отсутствие, чтобы решить, нужно ли ее добавлять в Release Notes.

  3. Если было 4 merge request'а, привязанных к 4 разным задачам, то в Release Notes так же попадет 4 задачи, все верно. Не вижу здесь проблемы.

  4. В таких ситуациях можно добавлять не все задачи, а только story. Обычно именно в них описываются бизнес-кейсы. Если story прилинкована к техническим задачам, то с помощью API Jira можно так же получить по ней информацию.

Ошибку поправили, спасибо.

Согласен на счет того, что можно парсить ID задачи еще и из ветки.

Спасибо!

С технической точки зрения задача не очень сложная. Правда, на мой взгляд, сложность внедрения решения обратно пропорционально размеру команды. Если у вас на проекте не много программистов, довольно легко будет объяснить им, почему следует добавлять ID задач Jira в сообщения коммитов. С другой стороны, если команда большая, то будут люди, которые начнут нарушать поставленные правила (либо по невнимательности, либо из желания протестовать). В этом случае, следует внедрить еще и git hooks.

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

Здесь вопрос именно в том, как люди используют Optional в Java. В Kotlin null-safety реализована на уровне компилятора, поэтому эти споры там неактуальны

По поводу первого примера: начиная с Java 9 есть удобный метод ifPresentOrElse.


Optional<Person> personOpt = findPerson();
personOpt.ifPresentOrElse(
    (person) -> {
        person.setXXX();
        updatePerson(person);
    }, 
    () -> {
        createPerson();
    }
)

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


Required, я считаю, не нужен, так в случае, если мы используем Optional, все является обязательным, кроме того, что в него обернуто. Если бы был еще и Required, то как когда интерпретировать типы данных без оберток?


С JPA в статье действительно ошибка. Я различаю Spring Data и JPA и имел в виду именно первое. Но, видимо, писал об одном, а думал о другом. Спасибо за замечание. Неточность поправлю.

Думаю, для таких случаев стоит использовать монаду Try

Согласен. Я использую статик-импорты, чтобы сократить код. Но для чего это разделение, мне тоже не понятно

Пример с инъекцией через Reflection в тесте – это то, как делать не надо :)

Вы все говорите правильно. Только давайте разберемся со "Smaller code is much easier to understand" и KISS. В моем понимании это означает, что класс/метод должны быть небольшими и делать одну конкретную работу. С этим я совершенно согласен. Но я не согласен с тем, что наличие конструктора делает мой код "громоздким".


Первое правило чистого кода — чем его меньше, тем лучше. Я не хочу городить конструктор и вызывать его вручную в тестах. Я предпочитаю стройный ряд объявлений зависимостей в самом начале моего класса.

Это неотъемлемая часть языка. Если они вас так не устраивают, можете использовать lombok или перейти на Kotlin. Там объявление главного конструктора объединено с объявлением полей. А если мне надо объявить дто? Тоже нельзя добавлять конструктор? Ведь это увеличит количество кода.


Хорошо, допустим я не использую конструктор в бинах Spring и полностью полагаюсь на DI через поля. Сейчас даже забудем про юнит-тесты и невозможность реализации BeanFactory. Пусть у меня появился другой класс. Не бин, а обычный Java-класс. Ему мне придется добавлять конструкторы/сеттеры, ведь Spring про него ничего не знает. То есть, получается, что у меня есть обычные классы и есть какие-то особенные классы, у которых заметно отличается декларация и вообще принцип работы с ними. Я не думаю, что это можно классифицировать как KISS. Как short, возможно, но не как simple.


В моем понимании KISS означает, что весь мой код должен быть написан в едином стиле.


Кстати, раз уж вы упомянули, что конструкторы – это дополнительный код. На мой взгляд, наличие большого конструктора – это сигнал того, что с вашим классом что-то не так. А если вы инжектите все через поля, не пишите юнит тестов, в которых необходимо создавать инстанс через new, то может возникнуть ощущение, что с классом все в порядке. В конце концов, можно в него добавить хоть 20 зависимостей. Это займет всего лишь 20 строчек кода, не так уж много. Справедливости ради, такое же может произойти и при использовании @RequiredArgsConstructor/@AllArgsConstructor

Прошу прощения, у какого Роберта Мартина вы прочитали такой вывод, что чистый код = меньше кода?) Тот факт, что мы создаем какие-то абстракции, интерфейсы, фабрики, разделяем приложение на разные слои, априори увеличивает количество кода.


По такой логике любой код на каком-нибудь Python автоматически становится более чистым, чем на Java.


Кстати, вы можете использовать lombok и @RequiredArgsConstructor вместо @Autowired над каждым полем. Тогда кода станет еще меньше, так как все @Autowired можно будет убрать)

Я знаю, что Spring для всех трех случаев использует рефлексию. Я не думаю, что это по-другому получилось бы реализовать, ведь даже если мы инжектим зависимости через конструктор, его надо как-то найти, затем понять, какие зависимости необходимо предоставить, а потом инстанцировать объект. Тут Reflection напрашивается сам собой.


Здесь вопрос скорее концептуальный. Насколько сильно мы хотим связывать наш объект с экосистемой Spring. Если взять все тот же доклад «Spring-потрошитель», то там Евгений использует аннотацию @InjectRandomNumber. Кастомный BeanPostProcessor находит ее и внедряет в поле случайное число. Мне лично такой подход не очень нравится, так как здесь получается слишком сильный coupling со Spring. Я бы вынес генерацию случайного числа в отдельный сервис и инжектил бы при необходимости. Также это сильно затрудняет понимания кода, особенно для новых членов команды. К тому же, здесь опять же упираемся в проблему юнит-тестирования.


Кстати, почему-то на этот аргумент против DI через поля никто не обращает внимания, но мой взгляд он не менее важен, поэтому продублирую его сюда из предыдущего комментария.


Да и проблема не только в тестировании. Как я уже писал в статье, в этом случае вы жестко привязаны к аннотации Component/@Service. Что, если потребуется инициализация через BeanFactory? Как передавать зависимости? Или вдруг класс вообще отвяжется от Spring, станет обычным типом. Все равно придется добавлять сеттеры/конструкторы. Так зачем полагаться на магию, если, просто используя стандартные подходы в Java, код становится чище и прозрачнее?

Я думал, что больше всего споров возникнет по поводу сеттеров. Как оказалось, field di до сих пор применяют. Хотя даже idea (новая версия, по крайней) предупреждает, что Autowired над полями использовать не рекомендуется

Если у вас зависимости инжектятся через поля, вы автоматически лишаете себя возможности написать unit-тест на данный компонент. Так как в таком случае просто нет способа, чтобы передать значение в объект без рефлексии.


Вы все ещё можете писать интеграционные тесты, поднимая Spring context. Но интеграционные должны расширять существующую тестовую базу, а не заменять собой модульные. К тому же, исходя из моего опыта, интеграционные тесты писать дольше и сложнее. И вряд ли кто-то будет обеспечивать покрытие в 80 процентов, если unit-тестов нет. Скорее всего, протестируют самый важный функционал, а на остальное закроют глаза.


Возможно, что у вас не так. Но это именно то, что я видел в жизни.

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

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

В приведенных примерах все поля private, так что я не понимаю, о каком присваивании без сеттеров вы говорите.


Сеттеры нужны только реально в случаях когда требуется логика при изменении поля, если просто бездумно их пихать там где не надо, то код превращается в мусор. Про тестирование вообще улыбнуло — автор явно не знает как тестируются спринг бины.

Согласен насчет сеттеров. Именно поэтому призываю использовать конструкторы. По поводу тестирования: я так понял, вы имеете в виду, что можно поднять контекст Spring в тесте и обойтись без сеттеров/конструкторов. Это будет интеграционный тест, про них я ответил выше.


Да и проблема не только в тестировании. Как я уже писал в статье, в этом случае вы жестко привязаны к аннотации @Component/@Service. Что, если потребуется инициализация через BeanFactory? Как передавать зависимости? Или вдруг класс вообще отвяжется от Spring, станет обычным типом. Все равно придется добавлять сеттеры/конструкторы. Так зачем полагаться на магию, если, просто используя стандартные подходы в Java, код становится чище и прозрачнее?
Опять же, DI через поля и сеттеры открывает дорогу к циклическим зависимостям.


«Spring использует Reflection API» — ну это вообще-то не всегда.

Насколько я знаю, для DI Spring всегда использует Reflection API. Ведь хотя для того чтобы найти классы, помеченные какой-либо аннотацией, нужна рефлексия. Если я в чем-то не прав, или не так вас понял, поправьте, пожалуйста.

То, что вы описываете, – это интеграционный тест. Хорошо, если они есть. Но они должны расширять тестовое покрытие, а не заменять собой модульные.

Могу сказать, что все это из личного опыта. Я видел код проектов, где до сих пор используется di через поля, даже не через сеттеры. Они даже крутятся в проде.

1

Information

Rating
6,000-th
Location
Москва, Москва и Московская обл., Россия
Works in
Date of birth
Registered
Activity