(java || kotlin) && devOps
369 subscribers
6 photos
1 video
6 files
306 links
Полезное про Java и Kotlin - фреймворки, паттерны, тесты, тонкости JVM. Немного архитектуры. И DevOps, куда без него
Download Telegram
Всем привет!

На собеседовании я иногда задаю вопрос: приведите пример нарушения принципа Single responsibility. Или альтернативный вариант - а вот если в методе, к примеру, activateCard мы заодно отбросим метрики или залогируем результат - это нарушение принципа или нет.
На первый взгляд ответ - нет. Метрики и логи - это технический код, не бизнес функционал. Он может понадобиться в любом месте кода. Часто такой функционал реализуют с помощью аспектов, т.к. во-первых - это можно реализовать с помощью аспектов, а во-вторых - это красиво))), т.е. некий синтаксический сахар, улучшающий читаемость кода.
Но можно рассмотреть немного другую ситуацию. Предположим, есть код с математическими вычислениям. Или любой алгоритм. Или логика обработки данных. То, что хорошо реализуется в функциональном стиле - входные данные метода, результат, никаких внешних зависимостей. В нём нет внешних взаимодействий, сохранения в хранилище. Чистая логика. В этом случае логирование и метрики - это уже некая обработка полученного результата. Мы же не просто так выводим что-то в лог - это либо данные для разбора ошибки, либо отслеживание пользовательского пути, сбор статистики, отслеживание времени выполнения кода... Т.е. есть отдельная логика по месту и составу того, что мы логируем. Опять же контекст логирования часто требует инициализации, что добавляет ненужные зависимости в нашу логику. Поэтому такой код лучше поместить на уровень выше.
Итого: бизнес функционал и логирование/метрики - да, "чистая" логика - нет.

#logging #metrics #interview_question #code_design #solid #dev_compromises
Всем привет!

Я уже поднимал тему boolean параметров как антипаттерна https://t.me/javaKotlinDevOps/229. Давайте расширим ее до вопроса - когда стоит использовать if?
Является ли if антипаттерном?
По мнению некоторых товарищей - да, является: https://www.antiifprogramming.com/about-the-anti-if.php
Как по мне - не всегда, зависит от ситуации.
Чем плох if? // да, switch - это по сути тот же if.

1) может нарушать принцип Single Responsibility. Почему - думаю объяснять не нужно.
2) может ухудшать читаемость кода, я которую я всегда "топлю") Т.е. нарушает принцип KISS. Усугубляет ситуацию тот факт, что код как правило не остается неизменным. И обычный if else со временем может превратится в многоуровневого нечитаемого монстра.
3) может нарушать принцип Don't Repeat Yourself. Тут два очевидных варианта - либо во всех ветках if выражения есть дублирующийся код, либо чтобы обработать возврат некого метода всегда нужен if.
4) если в коде слишком много if (x != null) - это признак того, что вы неправильно работаете с nullability. Тут могу посоветовать Kotlin, т.к. он может сообщать о null значениях на этапе компиляции. Optional и его альтернативы в Java избавляют от NPE, но не избавляет от проверок на null. Я видел советы - просто не пишите код, который возвращает null - тогда проверки будут не нужны. Но это надежда на человеческий фактор, и компилятор (я про Kotlin) работает лучше)))

Да, я специально пишу везде слово "может". Бывают if-ы, которые не нарушают ни один из принципов.

Когда стоит волноваться?

1) подключаем SonarQube или Checkstyle и не игнорируем ошибки, связанные с цикломатической сложностью методов, см. https://t.me/javaKotlinDevOps/197
2) код просто сложно становится читать. Особенно хорошо эта проверка проходит на новых разработчиках)

Идеально конечно не писать код, приводящий к лишним if. Но я уже писал про человеческий фактор выше)

Что можно сделать? // будет некоторый повтор написанного тут https://t.me/javaKotlinDevOps/229

1) выделяем сложный код условия в отдельный метод.
2) вместо двух или более веток оператора if делаем несколько методов. Помогает в случае, если условно метод А всегда вызывает метод С с значением true, а метод Б - с значением false. Иначе будет как на знаменитой картинке - проблема не на моей стороне)))
3) используем not null объекты и переходим Kotlin
4) перепроектируем код, чтобы проверки выполнялись в одном месте, а не дублировались по коду. Для этого их придется перенести из вызывающего кода в вызываемый. И придумать правильное значение по умолчанию.
5) при необходимости вводим иерархию классов, чтобы каждый класс отвечал за одну ветку switch
6) используем паттерн Стратегия - по сути частный случай введения иерархии классов
7) используем паттерн Состояние (State), который кроме хранения состояния выполняет обработку, связанную с различными состояниями, тем самым убирая if из вызывающего кода

#antipatterns #if_antipattern #java #kotlin #solid #patterns #dev_compromises
Всем привет!

В коде я часто вижу раздражающий меня паттерн - интерфейс с одной реализацией.
Почему так делается и когда так делать не стоит?

Для начала - когда полезны интерфейсы:

1) соблюдение принципа инверсии зависимостей, буква D из SOLID. См. https://t.me/javaKotlinDevOps/179 Высокоуровневые классы не должны зависеть от конкретных реализаций. На первый взгляд без интерфейсов тут никак. Да и на второй тоже) Но важно уточнить, что речь именно про базовые классы с логикой, т.наз. domain logic. Например, контроллера это не касается. Или адаптера к внешнему сервису.

2) соблюдение принципов гексагональной архитектуры. См. https://t.me/javaKotlinDevOps/232 Тут тоже есть нюанс - интерфейс и реализация должны находится в разных слоях приложения. Т.е. кейса, когда реализация лежит в подпакете impl рядом с интерфейсом это не касается. К слову, если мы говорим про слой бизнес-логики, то требования DI и гексагональной архитектуры - это одни и те же требования. И применимы они в основном для слоя бизнес-логики, она же предметная область.

3) Магии Spring проще работать с интерфейсами. Здесь важный момент - проще, но Spring вполне может работать и с классами без интерфейсов. Т.е. Spring или реализует интерфейс или наследуется от класса. Первое возможно всегда, второе - только если класс не финальный. И то, это важно для @Configuration, классов с методами, помеченными как @Transactional и @Async и возможно какими-то еще кейсами, о которых я забыл. Для Java просто не стоит делать их финальными. Для Kotlin - есть плагин kotlin-allopen, который сделает все за вас, а его актуальность увеличивает тот факт, что в Kotlin все классы по умолчанию финальные. И чтобы совсем уже не было вопросов - IDEA подсвечивает такие классы красным, предлагаю сделать их не финальными.

Почему если ваш случай не 1 и не 2 лучше не заводить интерфейс. Два аргумента:

1) меньше классов - лучше читаемость. А я за нее всегда топлю!) И за число строк кода не платят. Я надеюсь по крайней мере, что и у вас так)

2) мы не строим здания или мосты, мы пишем код. И при появлении второй реализации, например, тестовой, очень легко выделить интерфейс. Для этого есть готовый рефакторинг в IDEA. И еще есть рефакторинг переименование класса. Занимает 10 минут, шансов что-то сломать мало, особенно если есть достаточное покрытие кода тестами.

Вывод: интерфейсы полезны для соблюдения принципа Dependency Inversion и\или гексагональной архитектуры. Или когда есть хотя бы 2 реализации, включая тестовую. В остальных случаях нужно дважды подумать - нужны ли они вам.

#principles #dev_compromises
Всем привет!

Является ли ООП - объекто-ориентированное программирование - чем-то плохим? Ответ - ну нет. Благодаря объектам мы можем воспроизвести в коде реальные бизнес-объекты, а это стирает барьеры между заказчиком, аналитиком и разработчиком. Конечно, есть альтернативные подходы, например, функциональный. Или если посмотреть в другую сторону - декларативный. Но ООП жил, жив и будет жить)

Является ли архитектура сервиса, состоящая из нескольких слоев абстракции, какой-то излишней или неправильной? Нет, это стандартный подход в архитектуре - вводить новые уровни абстракции. Даже шутка на этот счет есть) Spring, Hibernate и куча других библиотек - это тоже новые слои абстракции. Цель введения нового слоя - упростить использование какой-то библиотеки или адаптировать ее для новой предметной области.

Что плавно подводит нас к паттернам Адаптер, Прокси и иже с ними https://t.me/javaKotlinDevOps/124 Паттерны - штука полезная, и да, я снова об этом уже писал https://t.me/javaKotlinDevOps/52 )))

И последний (риторический) вопрос: являются ли принципы DRY - Don't Repeat Yourself - и Single Responsibility вредными? Наоборот, они делают код более устойчивым к изменениям и упрощают его изучение.

Но почему же тогда в мире ПО не редкость встретить вот такую фабрику фабрик фабрик: https://factoryfactoryfactory.net ?

Ответ: ООП, принципы и паттерны не заменяют здравый смысл и чувство меры)

#oop #patterns #craftmanship #dev_compromises
Всем привет!

Недавно набрел на интересную статью - https://enterprisecraftsmanship.com/posts/domain-model-purity-completeness
Настоятельно рекомендую ее прочитать, но как всегда вкратце перескажу)

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

Варианта предлагается три:
1) внести ее в модель, скрыть обращение к данным за интерфейсами, но в любом случае в итоге наше ядро (модель) лезет в БД, что плохо. В первую очередь плохо концептуально, а если "спустится на землю" - сложнее писать модульные тесты, увеличиваются риски "загрязнения" ядра. Т.е. следующие поколения разработчиков видя, что из модели вызывается СУБД, скажут - а что, так можно было?) И будут тянуть в модель другие внешние зависимости. Теория еще такая есть, разбитых окон. К слову - автор статьи также автор отличной книги о модульном тестировании, я о ней уже писал https://t.me/javaKotlinDevOps/50, возможно поэтому ему данный вариант не нравится
2) оставить часть логики в контроллере. Но тогда получается, что логика размазана по двум слоям
3) заранее загрузить нужные данные в ядро. Допустимо, но только для каких-то маленьких и редко меняющихся справочников, типа регионов. Т.е. только в отдельных случаях.

В итоге у нас компромисс между полнотой модели, строгостью соблюдения архитектурных принципов и скоростью работы.
Что тут интересно - главная идея статьи не о том, как сделать правильно, а про то, что разработка ПО - это искусство компромиссов.
Теорема CAP, упоминаемая в статье, к слову о том же. Единственного правильного для всех случаев жизни решения нет. Увы(

Еще одно дополнение. В теории возможен еще один вариант, расширяющий пространство компромиссов. Он немного "наркоманский" в том плане, что усложняет систему.
Предположим мы сделаем интерфейс валидатора в модели. Правила валидации будет задавать декларативно и хранить в модели, не привязывая их к конкретному хранилищу. Т.е. код контроллера вместо:

validateLogic()
callModel()

превращается в:

val rules = getValidateRulesFromModel()
val request = buildValidateRequest()
validate(request)
callModel()

Сам же и покритикую. Задача вроде решена, но дополнительного кода потребуется много. Второй минус, тоже важный - последовательность вызовов неочевидна. Если новый разработчик придет в команду - очень может быть он скажет "WTF" и захочет переписать. Как решение этой проблемы могу предложить описывать алгоритм в документации к коду или аналитике. Документация не нужна с "говорящим" кодом, но тут как раз исключение. Но сложность понимания кода в любом случае повышается.
Т.об. в пространство компромиссов мы вводим еще один параметр - сложность. Полнота модели, целостность архитектуры, скорость и сложность.

#arch #unittests #dev_compromises
Всем привет!

Вытяну ссылку из комментариев сюда: https://youtu.be/hUzpe73Oa3g?si=c_dY1YU2Cc_F8YiY
Хороший ролик про границы применимости паттерна Value Object.

На всякий случай - что такое Value Object и чем он отличается от DTO - https://matthiasnoback.nl/2022/09/is-it-a-dto-or-a-value-object/

Также стоит отметить, что данный паттерн является одним из основных в DDD - Domain Driven Development.

А по видео у меня такой краткий вывод - а точнее два:
1) у любого паттерна есть своя область применения
2) когда вы придумали некий хитрый лайфхак, перед тем как реализовывать его в коде стоит взять паузу и подумать.
Насколько он понятен для новичка? Не усложнит ли он код? Насколько? Не станет ли поддержка такого кода сложнее? Не добавит ли он в вашу модель "уязвимость", позволяющую использовать классы и методы не так, как задумывалось изначально?
Часто лучше написать больше простого кода, чем меньше, но неочевидного и допускающего неверное использование. И далее либо разбить этот код на микросервисы, либо на модули - например, см. мой пост про Modulith - https://t.me/javaKotlinDevOps/143

#patterns #arch #dev_compromises
Всем привет!

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

Есть такой паттерн, по моему опыту проведения интервью самый известный после синлетона - фабрика. А точнее абстрактная фабрика, именно такой паттерн описан в классике - Design Patterns: Elements of Reusable Object-Oriented Software. Суть его в том, что функция создания новых объектов выносится в отдельный класс. Создаваемые объекты являются наследниками какого-то базового класса или интерфейса. Классов фабрик может быть несколько, при этом каждая фабрика создает все (!) необходимые для проведения некой операции совместимые (!) между собой объекты. Вот более подробное описание с примером https://habr.com/ru/articles/465835/

Т.е. на первый взгляд паттерн работает четко в соответствии с принципом единственной ответственности (Single Responsibility) - создание отделено от доменного объекта, более того, для разных потребителей процесс создания объектов разнесен по разным фабрикам - ToyotaFactory и FordFactory из статьи выше.

А теперь изменим пример из статьи - будем создавать не разные типы кузовов автомобилей, а детали автомобиля. А деталей в авто подозреваю несколько тысяч... И список их более изменчивый, чем список типов кузовов. Т.е. по сути объединив в одном классе создание нескольких объектов мы уже заложили мину замедленного действия. Где находится грань между работой по Single Responsibility и его нарушением?

Базовый ответ был в первом абзаце - все зависит от бизнес-процесса. Но попробуем добавить конкретики.
Для начала можно вспомнить про лайфхак - можно оставить в фабрике один метод и передавать ему на вход Enum с типом создаваемого объекта.
class Factory {
SomeItem createSomeItem();
OtherItem createOtherItem();
}

vs

class Factory {
Item createItem(ItemType type);
}
Он немного упрощает добавление новых классов, т.к. не надо менять API фабрики, но в итоге приводит к тому же результату. Но дает нам подсказку: когда в Enum становится слишком много элементов - значит с фабрикой надо что-то делать.
Еще вариант - посмотреть, что скажет SonarQube. Он предлагает ограничиться 35 методами и 750 строками кода для одного класса. Как по мне - это много, я бы начинал делить фабрику на части раньше, при появлении 10-15 методов или по мере появления логических сущностей, позволяющих взять группу связанных методов из большой фабрики и вынести их в отдельную фабрику.

#patterns #solid #dev_compromises
Всем привет!

Одна из моих любимых тем: разработка - искусство компромиссов. Поиск по тэгам #dev_compromises и #arch_compromises. Следствие этого подхода, принцип, который я бы назвал - "не все так однозначно".

Вопрос - как вы относитесь к рефлексии в Java?

Досрочный ответ: рефлексия - это плохо, лучше не использовать. Если дошло до рефлексии - значит в архитектуре проблема.

Чтобы лучше разобраться в теме надо ответить на вопрос: а почему плохо?
Ответа два:

1) рефлексия позволяет нарушить принципы ООП и, следовательно, архитектуру приложения. Автор скрыл внутренности класса через private, а мы туда лезем своими "грязными руками")))

2) снижение производительности. Тут частично работает тот факт, что делаются лишние вызовы кода. Но самое главное - JIT компилятор плохо умеет оптимизировать такой код, т.к. он слишком динамический. Изменится может сам класс, который приходит на вход метода с рефлексией

Окей, инкапсуляция нарушается, код работает медленно. Не используем?

А что с поиском аннотаций по коду? Не своих - до них мы еще дойдем - чужих, чтобы получить некие метаданные об объекте. Большинство вариантов вот тут основано на рефлексии https://www.baeldung.com/java-scan-annotations-runtime

Или у нас есть аннотации, созданные с помощью Spring AOP - это проще, чем AspectJ, если у вас используется Spring. А Spring AOP использует динамические прокси, создаваемые в runtime https://www.baeldung.com/spring-aop-vs-aspectj А с помощью чего создается прокси в runtime - правильно, рефлексии.

Да что там AOP - создание бинов из @Configuration - это тоже вызов методов @Bean через рефлексию.

Почему же рефлексию используют и предлагают к использованию, если это такая проблемная технология?

Вернемся к двум ее недостаткам:

1) не надо использовать рефлексию для вызова private методов или доступа к private полям. Если такая задача встала - у вас в самом деле проблемы с архитектурой

2) не надо использовать рефлексию часто и при этом в высоконагруженных приложениях. Тот же Spring использует рефлексию только при старте приложения для инициализации прокси и бинов. И к слову в т.ч. и поэтому старт Spring приложения может быть долгим, и люди с этим борются, см. мой цикл статей про ускорение старта #java_start_boost Более того, разработчикам Spring для поддержки native image пришлось серьезно допиливать его в т.ч. из-за динамических прокси и @Configuration https://docs.spring.io/spring-boot/reference/packaging/native-image/introducing-graalvm-native-images.html

Итого: рефлексию стоит рассматривать как возможность получить метаданные о классе. Помня при этом о производительности.

И если вопрос упирается в производительность - всегда есть альтернативы рефлексии. Это работа с байт-кодом не в runtime:
1) compile time (свой компилятор, см. статью про AspectJ)
2) post-compile time (свой плагин для сборки, см. Jandex для поиска аннотаций https://smallrye.io/jandex/jandex/3.2.2/index.html)
3) load-time (свой агент+classloader, см. статью про AspectJ)
Все варианты сложнее в реализации и подключении к проекту, зато вносят минимальное влияние в runtime.

P.S. Да, если при упоминании о динамических прокси вы вспомнили про задачку с собесов о вложенном @Transactional - это оно. И ответ на этот вопрос не так очевиден https://habr.com/ru/articles/347752/

#java #reflection #dev_compromises
Всем привет!

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

Мы все используем библиотеки. Когда нужно написать тест (а книга в основном про то, как упростить написание тестов для legacy) - часто в наш класс передаются библиотечные объекты. И тут могут быть две проблемы:
1) объект-синглтон
2) final объект или объект с методами, которые не возможно переопределить.
Эти две проблемы приводят к одному и тому же - мы не можем создать mock, приходится инициализировать для тестов реальный объект из библиотеки. А он может быть "тяжелым", превращающий модульный тест в интеграционный.

Как вариант решения этой проблемы предлагается:
1) для singleton создавать фасад, который разрешает замену объекта через setter, т.е. по сути нарушать суть паттерна singleton
2) для final методов и объектов - убрать final, объявляя их логическими final в документации.

Оба предложения по сути об одном - нарушаем принципы проектирования на уровне языка, зато делаем возможным тестирование кода.

В целом - "соль" в этом есть. Код без тестов опаснее кода, нарушающего принципы проектирования. Но я бы уточнил, что оба метода - это крайние меры. А по хорошему, если вы разрабатываете код, который кто-то когда-то будет тестировать - надо заранее озаботится о тестируемости, и именно:
1) не создавать singleton самому, использовать для этого IoC контейнер. Spring если библиотека внутренняя, и у вас используется Spring, или CDI аннотации в других случаях. Причем это актуально не только для разработчиков библиотек, а для всех.
2) создавать интерфейсы для классов, вынесенных в клиентское API. Есть интерфейс - всегда можно создать mock в тесте без всяких ухищрений. Я против создания интерфейсов всегда и везде https://t.me/javaKotlinDevOps/235, но Java API - это как раз подходящий случай. Одних интерфейсов конечно же мало, нужно еще и четкое разделение на модель и сервисы. Первые можно использоваться AS IS в тестах, вторые часто приходится мокать. Да, спроектировать все правильно не так-то легко, но как говорится - дорогу осилит идущий.

#book_review #ood #dev_compromises #libraries