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

Есть типичный пример плохого кода - большие процедуры, десятки или даже сотня строк каждая, неговорящие имена переменных, клубок вызовов, также известный как спагетти код. А тут прилетает задача - сервис нужно доработать, добавив какую-то важную проверку.
Есть техники для борьбы с плохим кодом. В первую очередь это собственно ООП - разделение кода по классам. Потом можно применить принцип единственной ответственности - каждый класс и метод отвечает за что-то одно. Далее идут принципы, описанные в книге "Чистый код" - понятные наименования, небольшие методы. Можно еще заполировать все это DDD - выделив ограниченный контекст. И разделить код по слоям, например, используя гексагональную архитектуру. Или даже разделить на микросервисы)
В результате получим десятки маленьких объектов, с четкой областью ответственности, хорошо читаемый код в каждом методе.
Но можно упустить важный момент - после выполнения всех доработок понять как работает система в целом (!) все равно будет сложно. Т.к. система стала слишком большой и сложной. И тут на приходит на помощь принцип KISS - Keep it simple. Перед тем, как начинать проектировать и кодить стоит задать вопрос - нужна ли еще одна валидация в коде? Может она уже есть в микросервисе, из которого мы получаем данные? Может вероятность появления таких данных в ПРОМ (PROD) стремится к нулю? Может можно подключить внешнюю библиотеку, где все уже сделано до нас?
В общем не забывайте про KISS)

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

В последние годы стала "модной" тема null safety. Суть в том, что не нужно хранить и передавать null значения, чтобы не напороться на Null Pointer Exception. В том же Kotlin null safety встроена в язык - все типы по умолчанию не могут содержать null.
И на самом деле это правильный подход. Но есть нюансы)

Рассмотрим такой случай - мы идем куда-то за данными, данные по бизнес-процессу там обязаны быть. Например, мы прихранили id записи где-то в пользовательском контексте в начале процесса и идем за данными в конце процесса. Но данных нет. Следуя null safety можно просто создать пустой объект - например, с помощью конструктора. Как вариант, часть полей этого объекта будет проинициализирована значениями по умолчанию.

Так вот - в случае, когда данных нет из-за какой-то нештатной редко воспроизводимой ситуации: неверные тестовые данные, на сервис идет атака с перебором всех возможных значений, в процессе операции данные некорректно мигрировали, кривая архитектура - лучше просто "упасть", т.е. выбросить исключение. Есть такой принцип - fail fast. Т.к. создавая пустой объект, мы во-первых надеемся что он будет корректно обработан выше, а это может быть не так. А во-вторых - а зачем передавать управление дальше?

P.S. Как всегда - напомню каждую ситуацию нужно рассматривать индивидуально, чтобы различать отсутствие данных как часть бизнес-процесса и нештатную ситуацию.

#kotlin #code #patterns #principles #nullsafety #fail_fast
Всем привет!

Если посмотреть на тему null safety с другой стороны - то это лишь один из аспектов более широкого принципа. Я бы сформулировал его как: "Знай свои данные".

Что же нужно знать:
1) кодировку текстовых данных - всегда явно ее указывать, не ждать, что всегда будет UTF-8
2) часовой пояс\locale - опять же не надеяться на то, что CI pipeline, тестовые и ПРОМ стенды работают в одном часовом поясе. А даже если это так - что на всех правильно настроен часовой пояс) И что никто не запусти код в другом часовом поясе. И никто не пришлет время из другого часового пояса
3) файловую систему, в частности разделители в пути к файлу. Для них есть специальные константы в Java Core, ОС в которой происходит запуск также можно определить
4) все проверки на injection - sql injection и аналоги
5) указание конкретных проверяемых исключений вместо Exception. Это не совсем данные, но часто исключения вызываются некорректными данными
6) unit тесты на граничные значения
7) ну и наконец null safety - может ли по бизнес-процессу значение принимать null или нет

Принцип можно расширить до инфраструктуры и добавить к примеру версии используемых библиотек и плагинов. Если версии не фиксировать - могут подтянутся новые в неожиданный момент с неожиданным поведением)

P.S. Если что-то забыл - дополняйте в комментах

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

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

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

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
Всем привет!

Я уже писал про то, что не люблю код, в котором интерфейсы делаются ради интерфейсов. Самый яркий антипаттерн: интерфейс с единственной реализацией, лежащей рядом. Подозреваю, одной из причин такого проектирования является принцип Dependency Inversion - в модели должны быть интерфейсы, а в сервисном слое - их реализации. И они разнесены по разным слоям приложения. Тут вроде все сходится?
Как бы да, но я нашел интересное возражение: https://habr.com/ru/articles/888428/
Как всегда побуду в роли ChatGPT) Суть статьи, а точнее продвигаемого там принципа структурного дизайна в том, что вместо создания интерфейса, любой сложный сервис можно отрефакторить (или спроектировать), выделив 3 метода:

1) чтение - все интеграции по чтению из внешних источников
2) бизнес-логика - тут должны быть чистые классы модели, принимающие на вход только простые типы или коллекции, без побочных эффектов
3) запись результата во внешние источники

2-я часть - это ядро, все остальное - imperative shell. Методы ядра вызываются из imperative shell, зависимости направлены в одну сторону.

Потенциальная проблема - возможно, читать придется много, чтобы загрузить данные для всех условий в бизнес-логике. Решение - дробить бизнес-логику на более мелкие части.
Еще проблема - как отследить, что в метод с бизнес-логикой не передали "побочный эффект". Например, класс, делающий вызов к БД - JPA lazy initialized DTO. Или ссылку на синглтон - Spring bean. Навскидку - только ручное код-ревью. Формально описать проверку объекта на отсутствие побочных эффектов сложно.
Огромный плюс - бизнес-логика всегда легко тестируется, даже без mock-ов.
Еще плюс - код проще.

Вот пример такого рефакторинга: https://www.youtube.com/watch?v=wq9LBouRULs

До чтения статьи, по заголовку, "покушение" на основы чистой архитектуры я воспринял критически. Но после ее прочтения и дальнейших размышлений идея понравилась.

Очевидно, она применима не всегда. Можно описать такую эвристику:

Простой сервис:
1) используем структурный дизайн, четко разделяем чтение с записью от бизнес-логики. С интерфейсами не заморачиваемся.
2) нужна вторая реализация какого-то сервиса - для тестового mock-а, динамической замены в runtime - легко вводим интерфейс с помощью IDE там, где он нужен

Сложный сервис, например:
1) библиотека
2) система с плагинами
3) коробочный продукт с несколькими реализациями под разных клиентов
4) наличие ядра, которое нужно защитить от правок "не доверенными лицами" -
изначально проектируем с интерфейсами в модели и реализациями в сервисах и портах\адаптерах

Принципу KISS - соответствует. Dependency Inversion - как ни странно тоже, т.к. в точной формулировке он звучит так: ядро не должно зависеть от деталей реализации. А структурном дизайне в ядро передаются только простые типы данных и коллекции с ними.

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

Прочитал сегодня хороший пост (английский): https://korshakov.com/posts/death-of-best-practices

Чем же он хорош?
Иллюстрирует 2 мысли, про которые я топлю в своем блоге (и топлю в целом):

1) разработка - искусство компромиссов. Важно не абсолютное следование ... микросервисной архитектуре, принципам Single Responsibility или Dependency Inversion. Нет никакой магии в цифре 80% когда мы говорим о покрытии кода тестами. Денормализация таблиц из той же оперы. Важно выдавать результат, требуемый бизнесов. На код-ревью можно поспорить о принципах, но Pull Request должен быть влит в течение дня

2) быстрые, а следовательно частые релизы - наше все. Или по-менеджерски - Deployment Frequency и Lead Time (а это две грани одной и той же идеи). Это позволяет быстро править баги и уязвимости, адаптироваться к изменениям архитектуры и нормативным требованиям, проводить бета и A\B тестирование, соблюдать сроки выхода фичей.

Что спорно в посте - кликбейтный заголовок. Читать "Чистый код" или "Мифический человеко-месяц" - это не зло. Зло - следовать догматически отдельным идеям из этих книг, не разбираясь, что хотел сказать автор, и не адаптируя их к текущему проекту.

P.S. Тоже что ли кликбейтные заголовки начать делать)

#principles #dev_compomisses