Продолжаю проводить технические интервью в компанию.
Немного заметок за 3 месяца:
1. В начале интервью стала лучше определять уровень кандидата, и теперь стараюсь задавать вопросы, отталкиваясь от уровня, а не просто пробегая по всем вопросам из списка.
2. Часто делаю акцент на Spring, т.к. вопросы по чистой Java у всех на слуху, и обычно кандидаты отвечают нормально.
3. При этом, иногда на популярных и вроде как простых вопросах по Java обнаруживается, что кандидат просто выучил на поверхностном уровне. А копнешь чуть глубже - уже понимания как такового нет. Поэтому такие вопросы тоже обязательно задаю.
3. Встречаю нередко, что кандидаты не понимают сложностей алгоритмов/используемых структур данных, и просто заучивают. Был случай, когда человек правильно рассказал сложности методов стандартных СД, но неожиданно не смог ответить про сложность вложенного цикла.
4. Очень интересно и ценно, когда кандидат не просто отвечает на вопрос, но приводит пример из своего опыта. Такое обычно встречаю у сильных кандидатов.
5. Стало проще соблюдать тайминг, и возвращать кандидата к теме, если разговор уходит в сторону. Но до сих пор порой выхожу за пределы стандартных полутора часов, т.к. хочется составить максимально объективное мнение. И бывает, что под конец интервью звучат именно те вопросы, которые повышают общую оценку.
Немного заметок за 3 месяца:
1. В начале интервью стала лучше определять уровень кандидата, и теперь стараюсь задавать вопросы, отталкиваясь от уровня, а не просто пробегая по всем вопросам из списка.
2. Часто делаю акцент на Spring, т.к. вопросы по чистой Java у всех на слуху, и обычно кандидаты отвечают нормально.
3. При этом, иногда на популярных и вроде как простых вопросах по Java обнаруживается, что кандидат просто выучил на поверхностном уровне. А копнешь чуть глубже - уже понимания как такового нет. Поэтому такие вопросы тоже обязательно задаю.
3. Встречаю нередко, что кандидаты не понимают сложностей алгоритмов/используемых структур данных, и просто заучивают. Был случай, когда человек правильно рассказал сложности методов стандартных СД, но неожиданно не смог ответить про сложность вложенного цикла.
4. Очень интересно и ценно, когда кандидат не просто отвечает на вопрос, но приводит пример из своего опыта. Такое обычно встречаю у сильных кандидатов.
5. Стало проще соблюдать тайминг, и возвращать кандидата к теме, если разговор уходит в сторону. Но до сих пор порой выхожу за пределы стандартных полутора часов, т.к. хочется составить максимально объективное мнение. И бывает, что под конец интервью звучат именно те вопросы, которые повышают общую оценку.
🔥5👍1
Testcontainers и неудачные тесты
В последнее много занимаюсь интеграционными тестами на тестконтейнерах.
Во время тестового прогона в какой-то момент получила ошибку - контейнер не создается из-за нехватки памяти на сервере. Подключилась к серверу, выполнила docker ps - а там крутится множество контейнеров kafka и postgres, которые я использую в тестах.
Потушила висящие контейнеры, тесты снова стали запускаться, но остался вопрос, что это было? Ведь тестконтейнеры должны автоматически останавливаться и удаляться после прогона теста. Опытным путем выяснила, что контейнеры не удаляются, если тест был остановлен принудительно.
Жизненным циклом тестконтейнеров управляет специальный контейнер ryuk. Он живет, пока живет JVM, и поэтому логично, при принудительной остановке JVM он экстренно завешает свою работу, не успевая остановить другие контейнеры.
Но неожиданно оказалось, что у коллеги, при точно таком же запуске и остановке тестов, контейнеры удаляются ryuk-ом.
Стала разбираться, почему у меня ruyk завершается в первую очередь, и не успевает удалить другие контейнеры, а у коллеги сначала удаляет, а потом уже сам останавливается? Оказалось, что это зависит от версии тестконтейнеров. Я использовала самую новую, 1.20.2, а коллега более раннюю.
Чтобы в будущем другие разработчики не сталкивались с непредсказуемыми ошибками, задокументировала это поведение в спецификации к родительскому тестовому классу.
В последнее много занимаюсь интеграционными тестами на тестконтейнерах.
Во время тестового прогона в какой-то момент получила ошибку - контейнер не создается из-за нехватки памяти на сервере. Подключилась к серверу, выполнила docker ps - а там крутится множество контейнеров kafka и postgres, которые я использую в тестах.
Потушила висящие контейнеры, тесты снова стали запускаться, но остался вопрос, что это было? Ведь тестконтейнеры должны автоматически останавливаться и удаляться после прогона теста. Опытным путем выяснила, что контейнеры не удаляются, если тест был остановлен принудительно.
Жизненным циклом тестконтейнеров управляет специальный контейнер ryuk. Он живет, пока живет JVM, и поэтому логично, при принудительной остановке JVM он экстренно завешает свою работу, не успевая остановить другие контейнеры.
Но неожиданно оказалось, что у коллеги, при точно таком же запуске и остановке тестов, контейнеры удаляются ryuk-ом.
Стала разбираться, почему у меня ruyk завершается в первую очередь, и не успевает удалить другие контейнеры, а у коллеги сначала удаляет, а потом уже сам останавливается? Оказалось, что это зависит от версии тестконтейнеров. Я использовала самую новую, 1.20.2, а коллега более раннюю.
Чтобы в будущем другие разработчики не сталкивались с непредсказуемыми ошибками, задокументировала это поведение в спецификации к родительскому тестовому классу.
👍6
Код и дизайн
Как понять назначение системы?
Раньше думала, что надо внимательно изучить код, тогда вся система станет автоматически ясна.
Но оказалось, это неверно.
Нужно изучить именно дизайн, документацию. Дизайн - это как раз верхнеуровневое описание системы. Одному дизайну может соответствовать множество реализаций. Поэтому из кода и нельзя понять назначение системы. Но в идеале разработчику нужно стремиться к такой реализации, чтобы ее невозможно было сопоставить ни с каким другим дизайном.
Для закрепления этого принципа решила переписать несколько классов из моего пет-проекта.
Для начала выбрала класс, который представляет собой генератор списка случайных координат. Эти координаты потом используются для построения маршрута на UI. Логика довольно простая - есть метод, который принимает начальную и конечную координату поездки, максимальную скорость и время, с каким интервалом генерируются координаты. Для получения списка точек нужно обратиться к внешнему API, преобразовать результат, а затем просто сохранять координаты в БД с заданным временным интервалом.
Уже на этапе формулирования этой простой логики возникли проблемы. Код был написан 2 года назад, и было сложно вспомнить, что он делает, т.к. метод представлял собой просто очень длинное полотно.
Но когда удалось вспомнить и сформулировать назначение кода, дело пошло быстрей.
Во-первых, оказалось, что логика обращения к внешнему API и преобразования полученного ответа очень тесно переплетены. Вынесла обращение к API в отдельный класс. Теперь в будущем без больших проблем можно заменить его на другой, если потребуется.
Вынесла различные вспомогательные методы в отдельные классы, например логику работы с датами - в класс DateUtil. Теперь метод генерации практически соответствовал логике дизайна, не разбухая от мелких технических деталей.
Циклы заменила на декларативные stream-ы, это сделало код еще более наглядным.
В итоге новый код был написан по заранее сформулированному дизайну, а не по наитию, как старый код.
При этом старалась, чтобы из реализации максимально точно можно было бы воспроизвести дизайн.
И старый, и новый код делают одно и то же, но новый оказался намного более понятным и читаемым.
Как понять назначение системы?
Раньше думала, что надо внимательно изучить код, тогда вся система станет автоматически ясна.
Но оказалось, это неверно.
Нужно изучить именно дизайн, документацию. Дизайн - это как раз верхнеуровневое описание системы. Одному дизайну может соответствовать множество реализаций. Поэтому из кода и нельзя понять назначение системы. Но в идеале разработчику нужно стремиться к такой реализации, чтобы ее невозможно было сопоставить ни с каким другим дизайном.
Для закрепления этого принципа решила переписать несколько классов из моего пет-проекта.
Для начала выбрала класс, который представляет собой генератор списка случайных координат. Эти координаты потом используются для построения маршрута на UI. Логика довольно простая - есть метод, который принимает начальную и конечную координату поездки, максимальную скорость и время, с каким интервалом генерируются координаты. Для получения списка точек нужно обратиться к внешнему API, преобразовать результат, а затем просто сохранять координаты в БД с заданным временным интервалом.
Уже на этапе формулирования этой простой логики возникли проблемы. Код был написан 2 года назад, и было сложно вспомнить, что он делает, т.к. метод представлял собой просто очень длинное полотно.
Но когда удалось вспомнить и сформулировать назначение кода, дело пошло быстрей.
Во-первых, оказалось, что логика обращения к внешнему API и преобразования полученного ответа очень тесно переплетены. Вынесла обращение к API в отдельный класс. Теперь в будущем без больших проблем можно заменить его на другой, если потребуется.
Вынесла различные вспомогательные методы в отдельные классы, например логику работы с датами - в класс DateUtil. Теперь метод генерации практически соответствовал логике дизайна, не разбухая от мелких технических деталей.
Циклы заменила на декларативные stream-ы, это сделало код еще более наглядным.
В итоге новый код был написан по заранее сформулированному дизайну, а не по наитию, как старый код.
При этом старалась, чтобы из реализации максимально точно можно было бы воспроизвести дизайн.
И старый, и новый код делают одно и то же, но новый оказался намного более понятным и читаемым.
Liquibase и тестовые данные
Приступив к задаче в новом микросервисе, столкнулась с необходимостью сгенерировать много тестовых данных. Можно было бы по примеру уже готовых тестов написать init-скрипт или создать нужные entity и сохранить их прямо в тесте.
Но нашлось более универсальное решение - настроить liquibase для тестовых сценариев.
У нас уже были тесты на тестконтейнерах, и они использовали liquibase, как и само приложение. Вначале показалось, что будет достаточно просто создать тестовый чейнджлог и миграции с тестовыми данными. Но возникли сложности - оказалось, что все таблицы создаются в схеме public, хотя в БД они живут в отдельной схеме. Это вызывало ошибки.
Помимо того, на проекте есть тесты как для postgresql c тестконтейнерами, так и тесты для h2. А в моей задаче добавились миграции с использованием системных таблиц postgresql. И конечно, при добавлении новых миграций тесты для h2 упали.
Разобралась как настроить liquibase для работы с кастомной схемой, добавила 2 тестовых чейнджлога - для h2 и для postgres, настроила профайлы для работы с разными БД. В результате все тестовые данные можно использовать для всех тестов, а добавлять их стало быстрее и проще.
Приступив к задаче в новом микросервисе, столкнулась с необходимостью сгенерировать много тестовых данных. Можно было бы по примеру уже готовых тестов написать init-скрипт или создать нужные entity и сохранить их прямо в тесте.
Но нашлось более универсальное решение - настроить liquibase для тестовых сценариев.
У нас уже были тесты на тестконтейнерах, и они использовали liquibase, как и само приложение. Вначале показалось, что будет достаточно просто создать тестовый чейнджлог и миграции с тестовыми данными. Но возникли сложности - оказалось, что все таблицы создаются в схеме public, хотя в БД они живут в отдельной схеме. Это вызывало ошибки.
Помимо того, на проекте есть тесты как для postgresql c тестконтейнерами, так и тесты для h2. А в моей задаче добавились миграции с использованием системных таблиц postgresql. И конечно, при добавлении новых миграций тесты для h2 упали.
Разобралась как настроить liquibase для работы с кастомной схемой, добавила 2 тестовых чейнджлога - для h2 и для postgres, настроила профайлы для работы с разными БД. В результате все тестовые данные можно использовать для всех тестов, а добавлять их стало быстрее и проще.
👍1
В продолжение предыдущей заметки.
Написала юнит тесты к очередной задаче. Тестируемый метод A был приватным, поэтому в тестах пришлось вызывать публичный метод B, вызывающий А. Метод А делает выполняет некую логику и сохраняет результат в БД. Вроде бы все просто, надо дернуть В, и проверить что в БД оказались нужные данные. Но на деле оказалось, что метод B вызывает также методы C, D, E... которые требуют еще данные для собственных расчетов. Когда я замокала все побочные вызовы, код выглядел мягко говоря, не очень понятно.
Решила применить тот же подход, что в предыдущей заметке - сделать интеграционные тесты с testcontainers.
Настроила liquibase, добавила тестовые данные, замокала пару вызовов... Тесты прошли успешно и стали выглядеть гораздо чище. Но в консоли обнаружилось несколько ошибок.
Оказалось, что приложение использует несколько схем в БД, и одна регулярная таска, которая запускается при старте приложения, пытается получить данные таблиц из разных схем и падает. И хоть на работу теста это не влияет, но некрасиво оставлять такие грязные логи. К тому же, если в будущем тесты упадут, это может запутать разработчика, который будет фиксить багу.
Liquibase для работы с кастомными схемами уже был настроен. Дальше разобралась, как настроить несколько датасорсов для работы с разными схемами. Теперь тесты стали полностью эмулировать работу приложения в слое обращения к БД. И можно легко создавать более сложные интеграционные тестовые сценарии.
Написала юнит тесты к очередной задаче. Тестируемый метод A был приватным, поэтому в тестах пришлось вызывать публичный метод B, вызывающий А. Метод А делает выполняет некую логику и сохраняет результат в БД. Вроде бы все просто, надо дернуть В, и проверить что в БД оказались нужные данные. Но на деле оказалось, что метод B вызывает также методы C, D, E... которые требуют еще данные для собственных расчетов. Когда я замокала все побочные вызовы, код выглядел мягко говоря, не очень понятно.
Решила применить тот же подход, что в предыдущей заметке - сделать интеграционные тесты с testcontainers.
Настроила liquibase, добавила тестовые данные, замокала пару вызовов... Тесты прошли успешно и стали выглядеть гораздо чище. Но в консоли обнаружилось несколько ошибок.
Оказалось, что приложение использует несколько схем в БД, и одна регулярная таска, которая запускается при старте приложения, пытается получить данные таблиц из разных схем и падает. И хоть на работу теста это не влияет, но некрасиво оставлять такие грязные логи. К тому же, если в будущем тесты упадут, это может запутать разработчика, который будет фиксить багу.
Liquibase для работы с кастомными схемами уже был настроен. Дальше разобралась, как настроить несколько датасорсов для работы с разными схемами. Теперь тесты стали полностью эмулировать работу приложения в слое обращения к БД. И можно легко создавать более сложные интеграционные тестовые сценарии.
🔥4
Решила освежить знания по паттернам ООП. Прочитала книгу Head First "Паттерны проектирования". Оказалась очень интересной. Действие происходит в городке Объектвилле, и все аспекты его жизни управляются паттернами —
пиццерия, шоколадная фабрика, метеорологическая станция и т.д.
Пару лет назад читала классическую GOF -- Design Patterns. Если их сравнивать, то GOF воспринимается как энциклопедия, а Head First скорее как художественный роман.
Теперь обе книги у меня в библиотеке.
пиццерия, шоколадная фабрика, метеорологическая станция и т.д.
Пару лет назад читала классическую GOF -- Design Patterns. Если их сравнивать, то GOF воспринимается как энциклопедия, а Head First скорее как художественный роман.
Теперь обе книги у меня в библиотеке.
🔥3👍1
В книге Head First "Паттерны Проектирования" не рассказывается про один из известных паттернов - Visitor (Посетитель). Он используется, когда мы хотим добавить новое поведение классам из иерархии, но не хотим сильно править сами классы.
Допустим у нас в проекте есть иерархия спортсменов различного уровня. Поступило требование, чтобы каждый спортсмен сдавал норматив по отжиманиям и подтягиваниям.
Для нас может быть нежелательно создавать новые методы в иерархии. Например, наши классы удачно спроектированы, покрыты тестами и мы не хотим ничего сломать.
К тому же, при внесении новых методов может нарушаться Single Responsibility Principle. Мы хотим сконцентрировать в классах код, который описывает занятие конкретным видом спорта, а нормативы - это просто побочная рутина спортсмена. К тому же вполне возможно что нормативы будут меняться, и каждый раз придется править классы иерархии.
С помощью Посетителя мы можем отделить новое поведение от данных.
А еще это можно сделать, используя дефолтные методы интерфейсов в Java.
Дефолтные методы - это реализация концепции примеси.
Примесь (mix in) - элемент языка программирования, реализующий выделенное поведение. Позволяет дополнить классы новым поведением, без необходимости наследовать класс от данного элемента.
Пост с примерами кода:
https://dmatveeva.github.io/visitor-default-method/
Допустим у нас в проекте есть иерархия спортсменов различного уровня. Поступило требование, чтобы каждый спортсмен сдавал норматив по отжиманиям и подтягиваниям.
Для нас может быть нежелательно создавать новые методы в иерархии. Например, наши классы удачно спроектированы, покрыты тестами и мы не хотим ничего сломать.
К тому же, при внесении новых методов может нарушаться Single Responsibility Principle. Мы хотим сконцентрировать в классах код, который описывает занятие конкретным видом спорта, а нормативы - это просто побочная рутина спортсмена. К тому же вполне возможно что нормативы будут меняться, и каждый раз придется править классы иерархии.
С помощью Посетителя мы можем отделить новое поведение от данных.
А еще это можно сделать, используя дефолтные методы интерфейсов в Java.
Дефолтные методы - это реализация концепции примеси.
Примесь (mix in) - элемент языка программирования, реализующий выделенное поведение. Позволяет дополнить классы новым поведением, без необходимости наследовать класс от данного элемента.
Пост с примерами кода:
https://dmatveeva.github.io/visitor-default-method/
👍2
Протокол FIX и библиотека QuickFIX/J
В последнее время много разбиралась с протоколом FIX и библиотекой QuickFIX/J.
FIX (Financial Information eXchange) — это стандартный язык для обмена финансовой информацией между участниками рынка. Он используется для передачи заявок на покупку и продажу, котировок и прочего. FIX обеспечивает быструю и надежную коммуникацию, что делает его основным инструментом в финансовой сфере.
Для работы с протоколом FIX существует множество библиотек, одной из самых популярных является QuickFIX/J. Это открытая реализация FIX на языке Java, которая позволяет разработчикам создавать торговые приложения, взаимодействующие с различными торговыми системами.
Как подключить?
Если у вас проект на Maven:
Вот пример простого клиента, который подключается к серверу FIX и отправляет тестовый запрос:
- стандартизация: FIX является общепринятым стандартом для финансовых сообщений.
- open-source: не нужно платить за библиотеку.
- гибкость: можно настроить всё под свои нужды.
В последнее время много разбиралась с протоколом FIX и библиотекой QuickFIX/J.
FIX (Financial Information eXchange) — это стандартный язык для обмена финансовой информацией между участниками рынка. Он используется для передачи заявок на покупку и продажу, котировок и прочего. FIX обеспечивает быструю и надежную коммуникацию, что делает его основным инструментом в финансовой сфере.
Для работы с протоколом FIX существует множество библиотек, одной из самых популярных является QuickFIX/J. Это открытая реализация FIX на языке Java, которая позволяет разработчикам создавать торговые приложения, взаимодействующие с различными торговыми системами.
Как подключить?
Если у вас проект на Maven:
<dependency>Дальше нужно настроить конфиг (например, config.cfg):
<groupId>org.quickfixj</groupId>
<artifactId>quickfixj-core</artifactId>
<version>2.3.2</version>
</dependency>
[DEFAULT]Это базовые настройки: кто вы (SenderCompID), с кем общаетесь (TargetCompID) и куда подключаетесь.
ConnectionType=initiator
SocketConnectHost=your.host.ru
SocketConnectPort=9876
SenderCompID=SENDER_ID
TargetCompID=TARGET_ID
[SESSION]
BeginString=FIX.4.4
Вот пример простого клиента, который подключается к серверу FIX и отправляет тестовый запрос:
public class FixClient implements Application {
public void onLogon(SessionID sessionId) {
System.out.println("Успешный логин: " + sessionId);
sendTestRequest();
}
private void sendTestRequest() {
try {
Message testRequest = new Message();
testRequest.getHeader().setField(new MsgType(MsgType.TEST_REQUEST));
testRequest.setField(new TestReqID("Тест"));
Session.sendToTarget(testRequest);
} catch (SessionNotFound e) {
e.printStackTrace();
}
}
}
Если разобраться с конфигами и самим протоколом FIX, то это удобный инструмент для автоматизации торговли и интеграции с биржами или брокерами. Его плюсы:- стандартизация: FIX является общепринятым стандартом для финансовых сообщений.
- open-source: не нужно платить за библиотеку.
- гибкость: можно настроить всё под свои нужды.
TDD
Последние 3 месяца активно практикую TDD в своей работе.
В этом посте опишу свой опыт.
📌 По классике процесс разработки с TDD должен выглядеть так: написание короткого теста, который падает, затем написание минимального кода, чтобы тест прошел, и затем рефакторинг написанного кода, без добавления новой логики. В идеале такие тесты, написанные перед началом разработки, должны служить своеобразной спецификацией, а код в результате все время следует этой спецификации.
〰️ Лично мне было немного сложновато следовать классической схеме, часто слишком увлекаюсь написанием кода и изменения в итерациях не всегда получаются минималистичными.
〰️ Также очень часто перед началом разработки хочется написать не короткий тест, а сразу большой, чтобы охватить все этапы тестового сценария, пока общая картина после чтения документации еще свежа и не размылась от погружения в технические детали. Но при следовании TDD, написание и выполнение теста должны быть быстрыми. Это значит что надо писать юнит-тесты, а в нашем проекте в основном используются интеграционные. Да и сама система спроектирована так, что сложно писать юнит-тесты, очень много приватных методов, а публичные часто принимают сложные объекты, завязанные на другие объекты.
〰️ Все это приводит к тому, что когда иногда интеграционных тестов становится слишком много, и они выполняются слишком долго. Пока размышляю над тем, как все-таки частично перейти на юнит-тесты.
Но несмотря на это, плюсов у TDD для меня оказалось намного больше.
✔️Во-первых, теперь к каждой задаче пишу тесты, что улучшает покрытие.
✔️Во-вторых, раньше зачастую недооценивала для себя время, нужное для написания тестов, и это выливалось в неверное планирование сроков. Теперь временные затраты оцениваю куда лучше.
✔️В-третьих, когда тест пишется после кода, сложно абстрагироваться для написания максимально непредвзятого теста. Все равно неосознанно опираешься на написанный код, и легко упустить краевые случаи.
✔️В-четвертых, появляется большая свобода в маневрах. Если по ходу разработки я хочу переписать свой код, то могу просто откатиться к предыдущему коммиту (которые надо делать после каждой итерации) и начать все заново. Не боясь, что большой кусок кода будет невозвратно сломан.
Подводя итог, для меня это крайне положительный опыт и очень продуктивная техника тестирования, которой буду придерживаться и дальше.
Последние 3 месяца активно практикую TDD в своей работе.
В этом посте опишу свой опыт.
📌 По классике процесс разработки с TDD должен выглядеть так: написание короткого теста, который падает, затем написание минимального кода, чтобы тест прошел, и затем рефакторинг написанного кода, без добавления новой логики. В идеале такие тесты, написанные перед началом разработки, должны служить своеобразной спецификацией, а код в результате все время следует этой спецификации.
〰️ Лично мне было немного сложновато следовать классической схеме, часто слишком увлекаюсь написанием кода и изменения в итерациях не всегда получаются минималистичными.
〰️ Также очень часто перед началом разработки хочется написать не короткий тест, а сразу большой, чтобы охватить все этапы тестового сценария, пока общая картина после чтения документации еще свежа и не размылась от погружения в технические детали. Но при следовании TDD, написание и выполнение теста должны быть быстрыми. Это значит что надо писать юнит-тесты, а в нашем проекте в основном используются интеграционные. Да и сама система спроектирована так, что сложно писать юнит-тесты, очень много приватных методов, а публичные часто принимают сложные объекты, завязанные на другие объекты.
〰️ Все это приводит к тому, что когда иногда интеграционных тестов становится слишком много, и они выполняются слишком долго. Пока размышляю над тем, как все-таки частично перейти на юнит-тесты.
Но несмотря на это, плюсов у TDD для меня оказалось намного больше.
✔️Во-первых, теперь к каждой задаче пишу тесты, что улучшает покрытие.
✔️Во-вторых, раньше зачастую недооценивала для себя время, нужное для написания тестов, и это выливалось в неверное планирование сроков. Теперь временные затраты оцениваю куда лучше.
✔️В-третьих, когда тест пишется после кода, сложно абстрагироваться для написания максимально непредвзятого теста. Все равно неосознанно опираешься на написанный код, и легко упустить краевые случаи.
✔️В-четвертых, появляется большая свобода в маневрах. Если по ходу разработки я хочу переписать свой код, то могу просто откатиться к предыдущему коммиту (которые надо делать после каждой итерации) и начать все заново. Не боясь, что большой кусок кода будет невозвратно сломан.
Подводя итог, для меня это крайне положительный опыт и очень продуктивная техника тестирования, которой буду придерживаться и дальше.
❤4✍3
Разобраться с кодовой базой
Работая на текущем проекте около полугода, задумалась, как эффективно разбираться с все более возрастающей кодовой базой. Под ответственность нашей команды передаются новые микросервисы, и часто новая задача = новый микросервис = новый процесс разбирательства с кодом с нуля. Простое чтение незнакомого кода и документации не позволяло качественно ускорить погружение в контекст и разработку новых фич.
Вот несколько шагов, которые помогли мне:
knowledge sharing по архитектуре
Смысл в том, что разработчик, который несколько знаком с сервисом, верхнеуровнево рассказывает о его процессах, об основных абстракциях, о потоках данных и преобразованиях с ними.
Плюсы таких встреч:
+ Рассказчик во время подготовки и объяснения улучшает свое понимание, обращает внимания на разные нюансы кода.
+ Слушатели узнают про архитектуру незнакомого микросервиса.
+ Тот, кто знаком близко с кодом данного сервиса, может внести правки или подсветить нетривиальные моменты.
В нашей команде была проведена серия таких встреч, и для меня это было чрезвычайно полезно.
Помимо рассказа, коллеги поделились диаграммами и схемами, которые тоже очень сильно помогают продвинуться в понимании .
Вопросы аналитикам
У нас ведется документация, но мне как разработчику часто не хватает полного видения.
Например, начиная разработку, я читаю в спецификации, что микросервис X должен отсылать сообщение в таком-то формате микросервису Y.
Если отправка прошла успешно, то сделать то-то, если вернулась ошибка, то другое.
Я могу просто написать такой код, но я буду знать о нем?
- Буду ли я понимать, для чего нужны сервисы X и Y?
- Что за процесс, для которого требуется отправка сообщения?
- Если мы получили положительный ответ, какие процессы начнутся в сервисе Y?
- Гарантирует ли положительный ответ то, что процессы в Y завершились успешно или лишь успешную доставку сообщения в Y? А почему?
Но найти ответы на эти вопросы в документации зачастую нетривиально. Они могут быть раскидана по разным разделам и страницам.
А поговорив с аналитиком, можно быстро получить информацию.
Конспект
После того, как получаю ответы на свои вопросы, стараюсь по возможности оформить их как конспект и сохранить в документации.
Плюсы такого подхода:
+ Информация лучше закрепляется
+ При необходимости легко вернуться и вспомнить что-то
+ Может быть полезно другим членам команды, особенно при онбординге
+ Сильно сокращает время ознакомления с информацией, т.к. часовое видео можно иногда уместить на пару страниц.
Все шаги были направлены на то, чтобы разобраться с кодом верхнеуровнево - получить целостное представление о системе, о процессах и преобразованиях данных в ней. С таким пониманием уже намного проще разбираться с конкретными классами, легко встраивать код в существующую систему и добавлять новый функционал.
Работая на текущем проекте около полугода, задумалась, как эффективно разбираться с все более возрастающей кодовой базой. Под ответственность нашей команды передаются новые микросервисы, и часто новая задача = новый микросервис = новый процесс разбирательства с кодом с нуля. Простое чтение незнакомого кода и документации не позволяло качественно ускорить погружение в контекст и разработку новых фич.
Вот несколько шагов, которые помогли мне:
knowledge sharing по архитектуре
Смысл в том, что разработчик, который несколько знаком с сервисом, верхнеуровнево рассказывает о его процессах, об основных абстракциях, о потоках данных и преобразованиях с ними.
Плюсы таких встреч:
+ Рассказчик во время подготовки и объяснения улучшает свое понимание, обращает внимания на разные нюансы кода.
+ Слушатели узнают про архитектуру незнакомого микросервиса.
+ Тот, кто знаком близко с кодом данного сервиса, может внести правки или подсветить нетривиальные моменты.
В нашей команде была проведена серия таких встреч, и для меня это было чрезвычайно полезно.
Помимо рассказа, коллеги поделились диаграммами и схемами, которые тоже очень сильно помогают продвинуться в понимании .
Вопросы аналитикам
У нас ведется документация, но мне как разработчику часто не хватает полного видения.
Например, начиная разработку, я читаю в спецификации, что микросервис X должен отсылать сообщение в таком-то формате микросервису Y.
Если отправка прошла успешно, то сделать то-то, если вернулась ошибка, то другое.
Я могу просто написать такой код, но я буду знать о нем?
- Буду ли я понимать, для чего нужны сервисы X и Y?
- Что за процесс, для которого требуется отправка сообщения?
- Если мы получили положительный ответ, какие процессы начнутся в сервисе Y?
- Гарантирует ли положительный ответ то, что процессы в Y завершились успешно или лишь успешную доставку сообщения в Y? А почему?
Но найти ответы на эти вопросы в документации зачастую нетривиально. Они могут быть раскидана по разным разделам и страницам.
А поговорив с аналитиком, можно быстро получить информацию.
Конспект
После того, как получаю ответы на свои вопросы, стараюсь по возможности оформить их как конспект и сохранить в документации.
Плюсы такого подхода:
+ Информация лучше закрепляется
+ При необходимости легко вернуться и вспомнить что-то
+ Может быть полезно другим членам команды, особенно при онбординге
+ Сильно сокращает время ознакомления с информацией, т.к. часовое видео можно иногда уместить на пару страниц.
Все шаги были направлены на то, чтобы разобраться с кодом верхнеуровнево - получить целостное представление о системе, о процессах и преобразованиях данных в ней. С таким пониманием уже намного проще разбираться с конкретными классами, легко встраивать код в существующую систему и добавлять новый функционал.
👍2❤1
Spring State Machine
Недавно в нашем проекте появился новый микросервис. После начальной конфигурации и добавления минимального функционала он стал быстро обрастать бизнес-логикой -- создание продуктов, процессинг, перевод продукта из статуса в статус. В других наших микросервисах за это отвечает непосредственно бизнес-логика, но для нового сервиса аналитики предложили внедрить Spring State Machine.
Из определения SSM:
"Spring State Machine — это фреймворк для Spring-приложений, который реализует концепцию расширенного конечного автомата. Он позволяет декларативно определять состояния и переходы между ними, поддерживает иерархические и параллельные состояния, а также действия, условия перехода и события."
Это показалось хорошей идеей: возможность формализовать логику переходов между состояниями и уйти от запутанных переходов в коде с многочисленными if...else if конструкциями. Задача ушла в разработку.
Когда я просматривала ее на code review, меня заинтересовал один технический момент. Пока я ресерчила этот вопрос, я внезапно наткнулась на сообщение о том, что Spring прекращает поддержку open source версии Spring State Machine с июня 2026! А значит через год библиотека SSM устареет и обновлений не предвидится. Но политика нашей компании обязывает обновлять версии, в которых обнаружены уязвимости. А поскольку обновлений нет, то библиотеку пришлось бы убирать из проекта.
А сколько кода было бы написано через год поверх SSM в нашем сервисе? И как больно было бы выпиливать его? Конечно, было принято решение не внедрять SSM в проект.
Для меня было сюрпризом, что Spring просто прекратил поддержку одного из своих проектов. Анонс о прекращении был 21 апреля 2025 года : https://spring.io/blog/2025/04/21/spring-cloud-data-flow-commercial
И что мне показалось интересно, я встретила свежие статьи в рунете с разбором SSM, которые даже не упоминали факт, что через год библиотека станет неактуальной. Похоже, стоит перепроверять на Github активность по каждой third-party библиотеке, которая добавляется в проект.
#рабочее
Недавно в нашем проекте появился новый микросервис. После начальной конфигурации и добавления минимального функционала он стал быстро обрастать бизнес-логикой -- создание продуктов, процессинг, перевод продукта из статуса в статус. В других наших микросервисах за это отвечает непосредственно бизнес-логика, но для нового сервиса аналитики предложили внедрить Spring State Machine.
Из определения SSM:
"Spring State Machine — это фреймворк для Spring-приложений, который реализует концепцию расширенного конечного автомата. Он позволяет декларативно определять состояния и переходы между ними, поддерживает иерархические и параллельные состояния, а также действия, условия перехода и события."
Это показалось хорошей идеей: возможность формализовать логику переходов между состояниями и уйти от запутанных переходов в коде с многочисленными if...else if конструкциями. Задача ушла в разработку.
Когда я просматривала ее на code review, меня заинтересовал один технический момент. Пока я ресерчила этот вопрос, я внезапно наткнулась на сообщение о том, что Spring прекращает поддержку open source версии Spring State Machine с июня 2026! А значит через год библиотека SSM устареет и обновлений не предвидится. Но политика нашей компании обязывает обновлять версии, в которых обнаружены уязвимости. А поскольку обновлений нет, то библиотеку пришлось бы убирать из проекта.
А сколько кода было бы написано через год поверх SSM в нашем сервисе? И как больно было бы выпиливать его? Конечно, было принято решение не внедрять SSM в проект.
Для меня было сюрпризом, что Spring просто прекратил поддержку одного из своих проектов. Анонс о прекращении был 21 апреля 2025 года : https://spring.io/blog/2025/04/21/spring-cloud-data-flow-commercial
И что мне показалось интересно, я встретила свежие статьи в рунете с разбором SSM, которые даже не упоминали факт, что через год библиотека станет неактуальной. Похоже, стоит перепроверять на Github активность по каждой third-party библиотеке, которая добавляется в проект.
#рабочее
👀1
Недавно столкнулась с ситуацией — один из сервисов на тестовом стенде не вычитывал сообщения из топика кафки.
Я убедилась, что сообщения точно попадают в топик, и что сервис вычитывает некоторые. Но не все.
И тут я заметила, что из топика почему-то читают пять консьюмеров, хотя экземпляров сервиса на тестовом стенде у нас всего три. Откуда еще два?🧐
Оказалось, что при обновлении config map для dev стенда прописали те же параметры кафки, что и для тестового стенда, и это экземпляры сервиса на dev стенде "крали" сообщения из топика.
Я убедилась, что сообщения точно попадают в топик, и что сервис вычитывает некоторые. Но не все.
И тут я заметила, что из топика почему-то читают пять консьюмеров, хотя экземпляров сервиса на тестовом стенде у нас всего три. Откуда еще два?🧐
Оказалось, что при обновлении config map для dev стенда прописали те же параметры кафки, что и для тестового стенда, и это экземпляры сервиса на dev стенде "крали" сообщения из топика.
😁3✍2
Обнаружила еще один плюс TDD.
Согласно подходу, я пишу тест, который падает, пишу код, чтобы тест прошел, и делаю коммит. Коммиты стараюсь делать как можно чаще. И обычно во время разработки я запускаю только этот тест, который пишу в данный момент, т.к. каждый раз прогонять абсолютно все тесты в проекте долго, особенно если там много интеграционных. Целиком тесты я запускаю только время от времени, если задача большая. Либо уже по окончании разработки перед пушем, если не очень большая.
И в какой-то момент по завершении фичи часть тестов упала, хотя я их даже не трогала. Благодаря TDD я быстро и без усилий обнаружила причину — просто откатила последовательно к каждому коммиту, и нашла, на каком эти тесты сломались. А т.к. коммиты относительно небольшие, проблему было легко локализовать.
Согласно подходу, я пишу тест, который падает, пишу код, чтобы тест прошел, и делаю коммит. Коммиты стараюсь делать как можно чаще. И обычно во время разработки я запускаю только этот тест, который пишу в данный момент, т.к. каждый раз прогонять абсолютно все тесты в проекте долго, особенно если там много интеграционных. Целиком тесты я запускаю только время от времени, если задача большая. Либо уже по окончании разработки перед пушем, если не очень большая.
И в какой-то момент по завершении фичи часть тестов упала, хотя я их даже не трогала. Благодаря TDD я быстро и без усилий обнаружила причину — просто откатила последовательно к каждому коммиту, и нашла, на каком эти тесты сломались. А т.к. коммиты относительно небольшие, проблему было легко локализовать.
Недавно занималась задачей, где нужно было реализовать оптимистическую блокировку сущности Hibernate при конкурентном доступе двух методов.
Задача была такая:
Сервис А обрабатывает таски. Во время обработки он делает запрос в сервис B. Бывает, что сервис B отвечает слишком медленно, и в таком случае нам хотелось бы, не дожидаясь ответа, отменять задачу и создавать другую с высоким приоритетом. Для этого я реализовала работающий по расписанию метод, чтобы он извлекал из БД и обрабатывал все задачи с истекшим временем ожидания.
Псевдокод:
В обоих случаях мы проверяем, что статус задачи корректный, и только потом продолжаем обработку. Но это не исключает ситуацию, когда одновременно пришел ответ от сервиса B, и истекло время ожидания задачи. И оба метода начали выполняться одновременно, и пока ни одна из транзакций не успела закоммитить изменение статуса. Теоретически можно получить вариант, когда наша задача в статусе DONE, а мы все равно создали PriorityTask, что нарушит консистентность данных. Чтобы решить проблему, можно использовать оптимистическую блокировку TaskEntity.
Для этого нужен атрибут version в таблице tasks в БД, и, соответственно, поле в сущности TaskEntity. Hibernate через это поле отслеживает изменения сущности. При сохранении он автоматически инкрементирует поле version, и выбрасывает OptimisticLockException, если поле в БД не совпадает с ожидаемым.
Поэтому, если транзакция из метода processTask закоммитит изменения первой, то мы получим исключение в методе processLongAwaitingTask, и его транзакция будет откачена. А если транзакция из processLongAwaitingTask закоммитит первой, то соответственно откатятся изменения processTask. Так сохранится консистентность данных.
Теоретически, есть другой вариант — использовать пессимистическую блокировку.
В этом случае только одна транзакция сможет получить доступ к строке из таблицы task, а другая будет ждать, пока первая не освободит данные. Пессимистические блокировки используются в условиях высокой вероятности конфликтов и сильных требований к консистентности данных, и могут замедлять процессинг и приводить к дедлокам. Но это не наш, случай, т.к. мы ожидаем, что конфликты будут редкими.
Задача была такая:
Сервис А обрабатывает таски. Во время обработки он делает запрос в сервис B. Бывает, что сервис B отвечает слишком медленно, и в таком случае нам хотелось бы, не дожидаясь ответа, отменять задачу и создавать другую с высоким приоритетом. Для этого я реализовала работающий по расписанию метод, чтобы он извлекал из БД и обрабатывал все задачи с истекшим временем ожидания.
Псевдокод:
// когда штатно пришел ответ от B
@Transactional
public void processTask(long id) {
TaskEntity task = taskRepository.findById(id);
if (task.getStatus() != IN_PROGRESS) {
return;
}
// some logic
task.setStatus(DONE);
taskRepository.save(task);
}
// когда время ожидания истекло
@Transactional
public void processLongAwaitingTask(long id) {
TaskEntity task = taskRepository.findById(id);
if (task.getStatus() != IN_PROGRESS) {
return;
}
task.setStatus(EXPIRED);
taskRepository.save(task);
PriorityTaskEntity priorityTask = new PriorityTaskEntity(...);
priorityTaskRepository.save(priorityTask);
}
В обоих случаях мы проверяем, что статус задачи корректный, и только потом продолжаем обработку. Но это не исключает ситуацию, когда одновременно пришел ответ от сервиса B, и истекло время ожидания задачи. И оба метода начали выполняться одновременно, и пока ни одна из транзакций не успела закоммитить изменение статуса. Теоретически можно получить вариант, когда наша задача в статусе DONE, а мы все равно создали PriorityTask, что нарушит консистентность данных. Чтобы решить проблему, можно использовать оптимистическую блокировку TaskEntity.
Для этого нужен атрибут version в таблице tasks в БД, и, соответственно, поле в сущности TaskEntity. Hibernate через это поле отслеживает изменения сущности. При сохранении он автоматически инкрементирует поле version, и выбрасывает OptimisticLockException, если поле в БД не совпадает с ожидаемым.
Поэтому, если транзакция из метода processTask закоммитит изменения первой, то мы получим исключение в методе processLongAwaitingTask, и его транзакция будет откачена. А если транзакция из processLongAwaitingTask закоммитит первой, то соответственно откатятся изменения processTask. Так сохранится консистентность данных.
Теоретически, есть другой вариант — использовать пессимистическую блокировку.
В этом случае только одна транзакция сможет получить доступ к строке из таблицы task, а другая будет ждать, пока первая не освободит данные. Пессимистические блокировки используются в условиях высокой вероятности конфликтов и сильных требований к консистентности данных, и могут замедлять процессинг и приводить к дедлокам. Но это не наш, случай, т.к. мы ожидаем, что конфликты будут редкими.
🔥1