(java || kotlin) && devOps
369 subscribers
6 photos
1 video
6 files
306 links
Полезное про Java и Kotlin - фреймворки, паттерны, тесты, тонкости JVM. Немного архитектуры. И DevOps, куда без него
Download Telegram
Всем привет!
Нашёл отличное сравнение скорости конкатенации строк разными методами, от + до StringBuffer, StringBuilder и StringJoiner. И даже есть такая экзотика как String.format со стримами.
Для затравки три интересных факта.
1) StringBuffer даже с синхронизацией существенно быстрее обычной конкатенации.
2) String.format очень(!) медленный.
3) скорость обычной конкатенации с увеличением числа строк растёт экспоненциально.
Подробнее тут https://www.baeldung.com/java-string-concatenation-methods
#java #performance #string
Всем привет!

Про DI и DI.

Аббревиатура DI может расшифровываться на Dependency Inversion, а может как Dependency Injection.

Dependency Inversion - это буква D из SOLID - базовых принципов разработки.
Означает, что высокоуровневые классы не должны зависеть от конкретных реализации, и в Java API любых классов лучше использовать интерфейсы везде, где это возможно. Почему такая ремарка: интерфейс с единственной реализацией - очень странная штука) Но я отвлекся) Следование принципу облегчает тестирование и расширение функциональности системы, т.к. позволяет легко заменить любую реализацию.

Dependency Injection - это механизм внедрения зависимостей, важнейшая особенность которого - собственно внедрение зависимостей отдается на откуп внешнему модулю. Самые известный пример - Spring c его IoC контейнером, но есть и другие заточенные конкретно на эту задачу и поэтому более шустрые альтернативы.

Если подходить формально - это два разных понятия, кроме аббревиатуры никак не связанные. Но с другой стороны Dependency Injection по сути - это инструмент, сильно облегчающий реализацию принципа Dependency Inversion. А хороший инструмент помогает писать правильный код. Важное замечание - Spring IoC не обеспечит за вас реализацию инверсии зависимостей. Если метод API завязывается на конкретную реализацию или уровни приложения связаны циклически - Spring тут не поможет. Поможет предварительное проектирование на уровне кода и TDD.

#code_architecture #interview_question #arch #patterns #solid
Всем привет!

Есть такой принцип - "безобразно, но единообразно". Если верить интернету, он появился в армии. Но может ли он быть применим к исходному коду и архитектуре?
Ответ - да.
Начну издалека - любой код и архитектура устаревает.
Но это не так страшно если выполняются три условия:
1) хорошо спроектированная гибкая архитектура. Я здесь про архитектуру уровня сервиса\АС. Маленькая ремарка - по отношению к коду термин архитектура плох тем, что в здании архитектуру изменить очень сложно. А в коде - можно.
2) высокое покрытие тестами. Тут все очевидно: много тестов - можно спокойно рефакторить
3) наличие времени для рефакторинга, т.е. другими словами - отсутствие вечного "это надо было сделать еще вчера"
В этом случае сервис можно отрефакторить для адаптации архитектуры к новым требованиям.
И делать это лучше сразу для всего приложения. Чтобы было "единообразно".
Почему так - я уже много об этом писал: изучать код - сложная задача. Один из способов облегчить ее - единообразие архитектуры, в т.ч. наименований, практики применения паттернов, разделения на уровни и модули.
Особенно сильно тяжесть изучения кода бьет по новичкам, но и для "старичков" спустя полгода-год код можно забыть.
В этом плане переделывать выглядящий "безобразно" код в отдельном методе или классе в рамках реализации текущей фичи - плохая идея, т.к. это ухудшит читаемость кода приложения в целом.
Лучше поговорить с бизнесом о рисках, зафиксировать техдолг, выделить на него отдельный технический релиз, согласовать время и все отрефакторить за раз.
Если вам кажется этот вариант фантастическим - понимаю, да, увы, такое бывает.
В этом случае предлагаю рефакторить максимально крупные куски кода вместе с бизнес-фичами. Ну например - модуль в терминах Maven или Gradle. Или набор модулей, относящийся к одному бизнес-процессу, если вы построили маленький монолитик. Или большой)
С монолитами, кстати, хуже всего - именно из-за устаревшей архитектуры, поменять которую разом невозможно, они зачастую и умирают.
При этом неплохо бы где-то рядом с кодом зафиксировать все архитектурные проблемы и план рефакторинга. В файлике типа todo.md в корне проекта. Точно не в wiki или в тикете, т.к. большинство разработчиков в wiki не пойдут.
Также подойдет JavaDoc. Часто он бывает тривиален и не несет ценности. Здесь же ситуация обратная.
Ну и конечно при ползучем рефакторинге поможет расстановка @Deprecated. На них ругается IDEA, SonarQube, они будут мозолить глаза. Это не гарантия того, что код будет поправлен, но уже что-то.
А лучший вариант - технический релиз. И не поосторожнее с монолитами)

P.S. Кстати, сложность рефакторинга является одним из возможных сигналов для разделения монолита на части

#arch #техдолг #refactoring #unittests
Всем привет!

Раз уж я начал говорить про SOLID - рассмотрим еще одну букву - O.
Open-closed principe - принцип открытости-закрытости.

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

Второй уровень - как можно сделать Open, т.е. расширить функционал? Наследование. Да, наследование - это хорошо. Но я отлично помню монолит с огромными иерархиями наследования, уровней по 10+. Бегать по этой иерархии искать где и что используется и точно ли я ничего не сломаю в новом наследнике - такое себе удовольствие. Да и SonarQube ругается, и правильно делает).
Альтернативы:
1) композиция и агрегация - объект базового класса можно включить в себя как поле, и уже в новом классе реализовать нужные требования. https://metanit.com/sharp/patterns/1.2.php
2) частный случай предыдущего пункта - паттерны декоратор и прокси, когда мы реализуем тот же интерфейс, что и базовый класс. См. разницу https://t.me/javaKotlinDevOps/124
3) комбинация аннотации и аспекта, реализующего дополнительное поведение над определенным методом класса
4) аннотации и создание прокси класса через CGLib или аналогичную библиотеку

Третий уровень - как правильно сделать Close? Закрыли мы класс для изменений, поставили final. А там конструкция вида:
private final List<String> POSIBLE_STORAGES = List.of("Oracle", "EhCache");
и далее везде в коде сохранения выбор из этих двух вариантов...
При желании наверняка обойти можно, но это значит, что автор класса не подумал про принцип открытости-закрытости.
Нужно было выносить в отдельный метод, а еще лучше - в отдельный объект Storage.

#arch #solid #inteview_question
Всем привет!

Продолжая тему SOLID. Есть в этой аббревиатуре буква L - принцип Барбары Лисков, который гласит, что все потомки должны соблюдать контракт, объявленный в родителе.
Объясню на пальцах. Есть класс или даже скорее интерфейс А. Есть класс Б, как-то использующий А - например, принимающий его как параметр в методе или конструкторе. И принцип Лисков будет соблюдаться в том случае, если класс Б не "упадет" при получении любого (!) потомка класса А, т.е. класс Б не должен разбираться в том, какого именно потомка А он получил.

На первый взгляд все просто - в Java и Kotlin строгая типизация и инкапсуляция, поэтому если мы объявили интерфейс с определенными методами и типами параметров - все потомки обязаны их реализовать. Что тут может сломаться?

Если копнуть глубже, то в Java наследники все же могут изменять контракт:
1) заменить тип возвращаемого методом результата на тип-наследник
2) добавлять в сигнатуру метода исключения-наследники и увеличивать число объявленных исключений, тоже наследниками
3) повышать область видимости метода
Все это расширяет ограничения в исходном контракте. Поломать корректно написанный класс Б, который знает только про А - интерфейс предка - так не получится. Хотя расширяя таким образом контракт, нужно быть осторожным, т.к. мы подталкиваем потребителей нашего нового класса к использованию конкретного класса вместо интерфейса, а это уже может привести к нарушению другой буквы - D )))

Как же можно точно нарушить контракт? Для этого нужно ответить на вопрос - что есть контракт?
Контракт не равно Java API. Это может быть:
1) назначение метода. Например, называя метод getXXX или checkXXX мы как бы говорим потребителю, что он не меняет состояние объекта, т.е. нет побочных эффектов
2) порядок вызова методов класса. Опять же исходя из названий методов мы можем предположить некий порядок вызовов - init() вызываем в начале, build() - в случае паттерна Builder в конце.
3) назначение класса. Если опять же исходя из названия класса он предназначен только для работы с кэшом - нужно в потомках лазить в БД, это должны быть разные сервисные классы. Если предполагается, что в сервисном классе данного уровня нужно отбрасывать логи и метрики - все потомки должны это делать. И наоборот.
4) политика по исключениям. Если предполагается, что метод возвращает внутреннюю ошибку как результат вызова (Pair или свой класс), то именно так и должны поступать все потомки
5) время выполнения. Если в контракте явно предполагается, что метод что-то рассчитывает in-process и не ходит во внешние системы - контракт нужно соблюдать.
6) зависимости от других классов. Нужно стремится к тому, чтобы все зависимости были явно указаны в API и передавались в класс при создании. Т.е. класс Б не должен как-то настраивать среду для работы конкретного потомка класса А. Да, речь про Inversion of Control.
7) иммутабельность. Если предполагается иммутабельная реализация - все потомки должны быть такими.

Как описать такой контракт? В первую очередь - в именах методов и классов. Если этого не хватает - в JavaDoc. Часто так бывает, что JavaDoc не несет новой информации для изучающего код, но в данном случае будет полезен.

И последняя очевидная, но IMHO важная мысль: чтобы наследники соответствовали контракту - для начала контракт нужно описать)

#arch #solid #interview_question
Всем привет!

Уже несколько раз упоминал в последних постах JavaDoc\KDoc. Нужен ли он вообще?
Мой ответ: да, но не всегда.
Основной критерий - JavaDoc должен приносить пользу. Т.е. давать дополнительную информацию. Дополнительную к чему?
1) названию модуля
2) названию пакета
3) названию класса\паттерна
4) названию метода или поля

Что это может быть?
1) контракт, как использовать и наследовать класс
2) причины выбора того или иного алгоритма
3) пример использования, его также можно поместить в unit тесты
4) TODO, причины объявления deprecated
5) ссылки на используемые в коде алгоритмы
6) краткое описание бизнес-логики, т.к. разработчики не ходят в wiki\Confluence )))
7) особые случаи использования, неочевидное поведение кода с некоторыми параметрами. Хотя это скорее антипаттерн, само наличие неочевидного поведения - повод подумать про рефакторинг

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

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

Когда-то на своей первой работе я услышал от коллеги совет: "Не надо бросаться реализовывать в коде первую попавшуюся идею. Подумай, найди пару альтернативных решений и сравни их".

Так вот, спустя более 15 лет практики хочу сказать - это очень важная и полезная мысль!)
По моему собственному опыту в 25-50% случаев первая пришедшая в голову идея не является оптимальной. Иногда приходится выкинуть все сделанное, иногда - сильно ее видоизменить. Причем часто это происходит еще в процессе работы над исходной задачей, при попытке вписать свой новый код в существующий сервис.
По сути речь идет о проектировании и его качестве. Когда нужно проектирование? Почти всегда, кроме исправления тривиальных багов - опечаток, замены текстовок например. А на самом деле даже простой в исправлении баг может указать на необходимость рефакторинга, а рефакторинг в любом случае требует проектирования.
Хорошим вариантом проверить качество идеи является практика TDD - в этом случае придется сразу написать типовые примеры использования вашего нового класса в виде тестов. Соответственно, сразу будет видно как новый код интегрируется с существующим, подходит ли для всех случаев использования.
При этом важно отметить, что TDD не заменяет проектирование.
Единственная альтернатива проектированию, которую я вижу - закладывать 1,5х или 2х времени для работы над задачей, чтобы была возможность все переделать) Альтернатива так себе, по моей оценке время на проектирование будет ниже.
Еще худший вариант: видя, что новый код придется переделывать - "докостылить" его до работающего варианта и записать себе задачу с техдолгом. К таким техдолгам, увы, долго не возвращаются.

Резюме - закладывайте время на предварительное проектирование по любой не тривиальной задаче. Ищите и сравнивайте альтернативные варианты, выбирайте оптимальный.

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

Я слишком ударился в архитектуру в последних постах, возвращаюсь с небес на землю)
Уже был пост как правильно писать unit тесты https://t.me/javaKotlinDevOps/33 и https://t.me/javaKotlinDevOps/43. Но кажется, что не хватает примеров. Начнем с антипаттернов - что не нужно делать в тестах?

Есть такая всем известная в Java мире библиотека для создания заглушек как Mockito. И у него есть среди всего прочего такая конструкция как

doNothing().when(mock).someMethod();

На первый взгляд полезная штука - говорим, что при вызове определенного метода в "заглушенном" объекте ничего делать не надо.
Но копнув чуть глубже понимаешь, что результатом вызова

val mock = Mockito.mock(SomeClass.class);

как раз и является объект, который ничего не делает.
А точнее метод объекта не вызывается, а там, где нужно вернуть результат - возвращаются некие значения по умолчанию: https://site.mockito.org/javadoc/current/org/mockito/Mockito.html#2
А если так - явно писать doNothing() - это усложнять код теста, его читаемость и ценность как документации к коду. Держать в уме, что все методы заглушки, которые не переопределены в тесте, не делают ничего - проще и понятнее, чем явно перечислять их.

Теперь уже метод doNothing() выглядит бесполезным. Зачем же его тогда добавляли в библиотеку?
Есть два кейса:
1) когда вы используете spy, а не mock. Напомню, главное отличие spy - он по умолчанию вызывает методы существующего объекта.
Пример:

List list = new LinkedList();
List spy = spy(list);
doNothing().when(spy).clear();
spy.someMethodWithCLeaCall();

2) и об этой возможности я, признаюсь, не знал - в одном выражении можно задать несколько ответов на ряд последовательных вызовов одного метода заглушки:

doNothing()
.doThrow(new RuntimeException())
.when(mock).someVoidMethod();

//первый раз ничего не делаем
mock.someVoidMethod();

//бросаем RuntimeException во второй раз
mock.someVoidMethod();

В следующих постах будет продолжение...

#unittests #mockito #antipatterns
Всем привет!

Продолжим про антипаттерны в unit тестах.
Мы можем проверить в тесте метод на то, что он выбрасывает исключение.
В JUnit 5:

assertThrows(MyException.class, () -> myObject.myFunction(myString), "Expected myFunction() to throw, but it didn't");

и с помощью AssertJ:

assertThatExceptionOfType(MyException.class)
.isThrownBy(() -> { myObject.myFunction(myString); })
.withMessage("%s!", "boom")
.withMessageContaining("boom")
.withNoCause();

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

Но есть еще методы проверки, что исключения не было.
В JUnit 5:

assertDoesNotThrow(() -> myObject.myFunction(myString));

и AssertJ:

assertThatNoException()
.isThrownBy(() -> myObject.myFunction(myString));

А вот эти проверки выглядят излишними, т.к. выбрасывание исключения в любом случае прерывает тест, и он подсвечивается красным.
Возможно, если тест сложный и в нем много проверок - хочется локализовать место ошибки. Но в этом случае я бы задумался про разделение теста на несколько вместо использования assertDoesNotThrow. Опять же, в противном случае придется использовать данную проверку во всех тестах, кроме тех, что выбрасывают исключение. А это IMHO "замусоривает" код.

P.S. Интересный момент - библиотека Truth, о которой я писал ранее https://t.me/javaKotlinDevOps/51, вообще не содержит методов проверки на выбрасывание исключения, предлагая пользоваться соответствующим методом из JUnit, ведь assertThrows, описанный ранее, возвращает исключение:

val exception = assertThrows(MyException.class, () -> myObject.myFunction());

А уже его можно проверить более детально:

assertThat(exception)
.hasCauseThat()
.isInstanceOf(NumberFormatException.class);
assertThat(exception)
.hasMessageThat()
.startsWith("Bad");

Мне этот подход нравится.

#unittests #junit #assertj #truth #antipatterns
Всем привет!

Еще один антипаттерн в unit тестах - чрезмерное увлечение verify.

List<String> mockedList = mock(MyList.class);
mockedList.size();
verify(mockedList, times(1)).size();

Да, штука полезная. Полезная по большому счету в 2 случаях:
1) проверка тестируемого метода в случае, когда он ничего не возвращает. К слову - еще одна альтернативная проверка: для mutable объекта и меняющего состояние метода проверить поля SUT (System under test).
2) проверка важных шагов алгоритма.
Если с void методом все понятно, то с деталями алгоритма главное - найти ту черту, которая отделяет суть алгоритма от деталей реализации. И проверять только первое. Т.к. в противном случае мы получим "хрупкий" тест, падающий при каждом изменении реализации. Собственно, от этого критерия можно и отталкиваться. Пример: если назначение данного сервисного класса вызов удаленной системы, отбрасывание метрик и логов - то именно эти вызовы и нужно проверить. В других случаях вызов метода логирования и отправку метрики я бы не проверял.

Еще хороший критерий - проверять те вызовы, которые описаны в аналитике.

Ясно, что 100% гарантии того, что тест проживет в неизменном виде долго и счастливо это не даст, мы же не провидцы) Но частота исправлений теста снизится.

Есть еще одна радикальная альтернатива - вообще не тестировать в рамках unit тестов методы, где все, что мы можем проверить - это вызывался тот или иной метод. А оставить это на интеграционные тесты.

#unittests #antipatterns #mockito
Всем привет!

Еще одна проблема, которую я замечаю в unit тестах - это засилье copy-paste. Тесты есть, покрытие хорошее, тесты проходят. Все хорошо?
Не совсем.
Как я уже писал тест - это самая лучшая документация к коду. Поэтому если в тесте есть mock объект, скопированный с другого теста, но на самом деле не нужный для успешного прохождения теста - это вводит в заблуждение при изучении проекта. Аналогично - если есть лишние проверки verify, которые конкретно у данного теста всегда успешны или не проверяют ничего полезного, зато приводят к хрупкости теста.

Резюме: тесты - это тоже код, к их написанию нужно относится с той же серьезностью, как и к коду.

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

Есть такая отличная штука в Kotlin, как контекстные функции.
Вот документация https://kotlinlang.org/docs/scope-functions.html
Вот пример, хорошо иллюстрирующий зачем они нужны:

val man = Person("Vasya").apply {
age = 20 // same as this.age = 20
city = "Moscow"
}

Код стал проще, читается хорошо. Если, конечно, ты знаешь про контекстные функции)

Но как и любая вещь, контекстные функции могут быть использованы не только во благо(
Вот несколько антипаттернов:

val params = claim.systemState?.let {
FailureParams(
claim.partnerName,
it.name,
)
}

Что мне здесь не нравится - читаемость. Если читать сверху вниз, то получается, что мы берем статус из заявки и присваиваем переменной params ... не его, а совершенно другой объект, созданный внутри let. Скорость понимания кода страдает.

return claim.also {
saveToCache(it)
}

Опять же, мне не нравится читаемость кода. Мы возвращаем результат метода, claim. А нет, не возвращаем. Вначале пишем его в кэш. А потом уже возвращаем.
Гораздо проще было бы:

saveToCache(claim)
return claim

Ну и наконец самый хит:

return claim.also {
saveToCache(someOtherObject)
}

Зачем? Почему? Не понятно)

P.S. По ссылке выше есть неплохая табличка https://kotlinlang.org/docs/scope-functions.html#function-selection Это по сути навигатор по контекстным функциям - позволяет выбрать одну из 6 для вашего конкретного случая. На первое время точно будет полезной

#kotlin #antipatterns
Всем привет!

Сегодня пост о крутой фиче Kotlin, которая решила одну важную проблему. И добавила другую)
Я о Null safety.
Суть ее в том, что в Kotlin любой тип представлен в двух ипостасях - одна может содержать null значения, другая - нет.
String - не может содержать null,
String? - может.
Это два разных типа, неявное приведение второго к первому "без приседаний" невозможно.
Что дает Null safety?
По умолчанию предлагается использовать тип not null, и если так и делать, то кажется, что про NPE - NullPointerException - можно забыть. А заодно забыть о проверках на null как в коде, так и в тестах. Небольшой оффтоп - проверка not null значений на null в тестах - еще один антипаттерн. Говорит о том, что пишущий этот код еще не до конца познал Kotlin)
Вроде бы все хорошо. Но есть нюанс. Что будет, если присвоить not null переменной nullable значение? Ошибка компиляции. А всегда ли компилятор знает, что это nullable значение? Если тип Kotlin - то всегда. Если тип Java - то в общем случае не знает. Что же он делает в таком случае? А ничего, просто разрешает присваивание.

//Java
public class JavaNullability {
private Boolean value;

public Boolean getValue() {
return value;
}
}

// Kotlin
class KotlinNullability {
constructor(test: JavaNullability) {
val nullValue: Boolean? = test.value
val notNullValue: Boolean = test.value
// компилятор разрешает оба варианта
}
}

Что же будет в runtime? Привычный нам в Java NPE на втором присваивании. Да, null safety не защищает от NPE в Kotlin коде.

Что тут можно сделать?

1) при взаимодействии Kotlin-Java знать nullability Java параметров, с которым мы работаем. Это может быть описание контракта - OpenAPI спецификация или любой другой контракт.
2) явная проверка на null в коде присваивания
3) самый плохой вариант, антипаттерн - всегда присваивать значения из Java nullable типам в Kotlin. Почему это плохо? nullable типы расползаются по всей программе, и в итоге мы теряем все преимущества Kotlin null safety.

P.S. Но в целом: null safety - это круто! Подробнее можно о ней можно почитать в официальной документации: https://kotlinlang.org/docs/null-safety.html#nullable-receiver

#kotlin #java #antipatterns #nullsafety
Всем привет!

Чтобы проиллюстрировать предыдущий пост - предлагаю сравнить по критерию null safety три языка: Groovy, Java и Kotlin.
Тестовая задача такая - передать null Boolean значение в конструктор и проверить его в if.

Groovy

import groovy.transform.TupleConstructor

@TupleConstructor
class GroovyNullability {
Boolean value

void checkValue() {
if (!value) {
println 'groovy value is false'
}
}
}

Т.к. в Groovy все, что не true считается false - вызов checkValue распечатает строку.

Java

public class JavaNullability {
private Boolean value;

public JavaNullability(Boolean value) {
this.value = value;
}

public void checkValue() {
if (!value) {
System.out.println("java value is false");
}
}
}


Вызов checkValue упадет c NPE: "Cannot invoke "java.lang.Boolean.booleanValue()" because "this.value" is null" на преобразовании Boolean к boolean использования в логическом выражении (boolean expression).

Kotlin

class KotlinNullability {
var value: Boolean = false

constructor(test: Boolean?) {
value = test!!
}

fun checkValue() {
if (!value) {
println("kotlin value is false")
}
}
}

Чтобы присвоить null значение not null переменной придется пойти на хитрость - через !! указать, что мы уверены, что в nullable переменной не будет null значений. И если же null все же придет - тоже будет java.lang.NullPointerException, причем без подробностей. Но на шаг раньше, еще на присваивании not null переменной.
К слову - если передать Java объект с null значением, через класс-обвертку:

constructor(test: JavaNullability) {
value = test.value
}

NPE будет чуть более подробным: "java.lang.NullPointerException: test.value must not be null"

#kotlin #java #groovy #nullsafety
Резюмирую предыдущий пост:

1) в Groovy нет null safety
2) в Java проблемы с null проявятся при попытке работы с null объектом, в т.ч. при неявных преобразованиях
3) в Kotlin с использованием Java библиотек проблемы будут проявляться при присваивании в Kotlin коде
4) чистый Kotlin = null safety

#groovy #kotlin #java #nullsafety
Всем привет!

Если посмотреть на тему 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
Всем привет!

Постов долгое время не было, причина типичная - много работы. Вообще не помню времени, когда ее было мало((( И были ли вообще такие времена?)

Хотел бы поднять сегодня такую важную тему как взаимодействие разработчиков и сопровождения.
Для начала одна общеизвестная информация - разработчики и сопровождение исходя из своих задач обречены на противостояние.
Задача разработчиков - менять приложение, задача сопровождения - обеспечивать его работоспособность. А как известно: работает - не трогай) Любое изменение потенциальный источник проблем.
Отсюда часто следует одна крайность - сопровождение максимально критично относится к любым изменениям, разработка "виновна" по умолчанию, требуется строгое соблюдение регламентов, любая нестандартная просьба разработки встречается "в штыки".
Почему это плохо?
Сейчас основная методология разработки - это Agile в разных вариациях. Один из ключевых моментов в Agile - это команда, командная ответственность, гибкие решения в команде. А сопровождение в описываемом кейсе выступает внешним "врагом" - блокирует инициативы команды, замедляет скорость выпуска новых версий. А высокая частота выхода в ПРОМ - еще одна важная часть Agile.
С таким кейсом я, увы, сталкивался и наблюдал его губительный для команды эффект. Часто свои требования сопровождение объясняет требованиями надежности. Хорошее ли это объяснение - зависит от деталей. Если объяснение звучит как-то так - это снизит надежность, т.к. ... и идет описание причин - то да, хорошее.
Если слово надежность произносится, а никаких деталей не приводится - это признак того, что сопровождение боится изменений и не хочет развиваться.
Есть и другая крайность - сопровождение "согласно на все") Не выставляет никаких требований, принимает любые дистрибутивы. Кейс более редкий. Чем это плохо - разработчики опять же исходя из своих основных задач редко думают о том, как их код будет сопровождаться. Обычно на разработку времени хватает впритык.
Какой выход из данной ситуации?
Выставлять требования со стороны сопровождения.
Требования зависят от компании, отрасли, приложения, числа пользователей и много чего.
Но базовые требования могут быть такими:
1) список метрик, позволяющих отслеживать работоспособность
2) требования к логам - где и в каком объеме. Сюда же я бы добавил требования к фильтрации логов, с важным дополнением - возможность фильтрации зависит как от разработчиков бизнес-приложения, так и от разработчиков системы просмотра логов
3) требования к трассировке (tracing) - особенно важно если мы имеем дело с микросервисами
4) наличие инструкции для сопровождения в случае, если установка релиза требует ручных действий
5) наличие сценария отката на предыдущую версию. Это может быть выключение feature toggle или номер версии для отката. Самое важное - сама возможность отката. Это тоже требование, его нужно или соблюдать, или если это невозможно, например, в случае необратимых изменений в БД - составлять план действий на случай неудачной раскатки
6) фиксирование таймаутов внешних вызовов: я уже писал что бесконечные таймауты - одна из основных причин падения приложения
7) требования по UI для ручного разбора ошибок, если все предыдущие требования не помогли)

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

#dev #ops
Всем привет!

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

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

Может ли API с широким функционалом стать проблемой? Имея несколько реализаций от разных производителей ПО, являясь эталонной реализацией паттернов интеграции https://www.enterpriseintegrationpatterns.com/ при этом быть хуже конкретного продукта с ограниченым набором функционала?
Как можно догадаться из вопроса - ответ: да. Я про JMS vs Kafka.

JMS - это API из состава Java EE (сейчас Jakarta EE). Есть несколько реализаций: практически у каждого сервера приложений есть свой JMS - IBM, Oracle, JBoss, SAP, есть и Open source решения - ActiveMQ, Artemis MQ и другие.
Что есть в JMS? Стандарт широкий: есть очереди (точка-точка, ака P2P) и топики (подписка, ака PubSub), опциональная персистентность и транзационность, возможность настраиваемой маршрутизации и конвертации сообщений. И с security все хорошо.

У Kafka же только топики, нет продвинутой маршрутизации, трансформации, персистентность постоянная, транзакционности нету, гарантии однократной вычитки должен обеспечивать клиент. Да и вендор один. Но справедливости ради vendor lock нет, т.к продукт open source, за деньги только поддержка.

При этом Kafka успешно отвоевывает долю рынка. В чем же дело?

Секрет Kafka в том, что она выпустила достаточно простой, очень быстрый open source продукт, достаточный для большого числа клиентов. Этим нивелируется преимущество JMS в возможности выбора реализации. Причем Kafka быстрая с включённой по умолчанию персистентностью, а значит и высокой надёжностью. По скорости однозначно бьет все реализации JMS. Про то, как удалось добиться такого результата, я писал тут https://t.me/javaKotlinDevOps/91

В чем проблемы JMS:
попытка объять необъятное в API = переусложнение,
много вендорской специфики, которая может помешать смену реализации,
слишком большая роль брокера - все возможности по маршрутизации и трансформации не бесплатны по производительности, и кроме того ведут к тому, что обычная очередь превращается в Enterprise Service Bus, а у этой концепции есть свои минусы.

Я не хочу сказать, что JMS можно выкидывать, а лишь пишу почему Kafka удалось ее так сильно потеснить. Если вам нужно взаимодействие точка-точка и нет больших объёмов данных - JMS вполне подойдёт.

#kafka #jms #comparison
Всем привет!

Я уже писал о важности рефакторинга.
Но как оценить, что рефакторинг достиг своих целей?
Можно экспертно. Но не всегда этот вариант годится для обоснования времени на рефакторинг у бизнеса.
Можно ли как-то подтвердить эффект цифрами?
Самый простой вариант - если проблема в производительности. Тогда изменяем TPS - transaction per second - до и после и демонстрируем эффект.
А если проблема в сложности кода как это чаще всего бывает?
Тогда на помощь могут прийти следующие метрики:
- цикломатическая сложность кода и другие похожие метрики https://www.tiobe.com/knowledge/article/demystifying-code-complexity/ После рефакторинга сложность должна снизиться, при этом в процессе может временно повышаться. А стандартные метрики хороши тем, что уже есть утилиты для их подсчёта. Например, Checkstyle https://checkstyle.org/checks/metrics/cyclomaticcomplexity.html или Sonarqube https://habr.com/ru/articles/565652/ Также благодаря тому, что эти метрики широко известны в узких кругах разработчиков )) их проще объяснить бизнесу. Или пусть изучают формулу, или верят на слово разработчикам)
- если сложность кода по каким-то причинам не подходит - можно рассмотреть среднее число строк в классе или методе. После рефакторинга это число также должно снижаться,
- в некоторых случаях может даже подойти абсолютное число строк кода. Если приложение «раздуто», то результатом рефакторинга станет уменьшение размера кодовой базы.
- число TODO в коде. Каждая TODO - потенциальный техдолг.
- число длинных комментариев в коде. Если код самодокументирующийся - с говорящими названиями методов и классов - то длинный комментарий можно рассматривать как аналог TODO
- скорость вникания в код сервиса для новых разработчиков. Изменить достаточно трудоёмко, но метрика хорошая
- число ошибок ПРОМ, связанных с изменяемым кодом. Если их много - значит с кодом что-то не так
- постоянно увеличивающееся или стабильно большое время разработки новых фичей. Например, по сравнению с временем в начале жизненного цикла сервиса. Сравнивать с другими командами и сервисам часто не корректно.

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

#java refactoring #metrics