03/02/2023

CI, кодстайл и TDD: обзор практик для повышения качества

Я видел не во сне, а наяву атакующие корабли, пылающие под четырьмя вложенными if-else, и лучи CI с кучей сканирований у ворот Тангейзера, вызывающие лютую боль разработчиков. Меня зовут Максим, и я техлид в Газпромбанке.

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

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

В примерах будет код на Kotlin, однако это не означает, что практики нельзя применить к другим языкам. Руководство подойдёт даже для YAML-программистов. Попробуйте после прочтения материала посмотреть на какой-нибудь код ролей Ansible, и наверняка обнаружите простор для улучшения.

Код для людей

1. Пишите код и тесты так, чтобы они были главной документацией разработчика

Ваш код должен выглядеть и читаться как основная документация. Это не значит, что аналитическую и бизнес-документацию надо выбросить в мусорку. Такие материалы должны дополнять тесты и кодовую документацию как спецификация для сверки.

Как достичь такого «самодокументированного» кода? Начните с постепенного введения Type-Driven Development и тестов бизнес-логики. В случае с Kotlin: делайте sealed class и в нём описывайте все возможные результаты выполнения метода через классы и object’ы.

Вот пример:
Type Driven в лучах ValueObject
Суть в этом — класс сам себя тестирует:
Нюансы:
  • Внедрение Type-Driven Development сопряжено с пусть и небольшими, но затратами.
  • Требует перемен в складе мышления.
  • Не совсем понятно, что в Type-Driven Development делать с исключениями. Но мы к этому ещё вернёмся.
2. Логи и мониторинг в помощь инженерам

Что лучше — писать логи в какой-то файл где-то на файловой системе или отправлять их в stdout? Отправлять логи строками в Elasticsearch или потратить время на изучение distributed tracing и структурированных логов? Спроектированный с умом мониторинг сокращает время обратной связи и упрощает поиск причины неполадок там, где нет дебаггера под рукой.

Как можно внедрить логи с метриками на примере незамысловатого класса-прослойки для чтения файла. Подключить сбор метрики: alphaMetrica (productId, route).

В коде это будет выглядеть так:
Всё должно быть прозрачно, очевидно и настраиваемо.

Особенности:
  • Делать логи надо такими же читаемыми, как и код, т. е. избегать println («123»).
  • Метрики лучше готовить вместе с инженерами эксплуатации, иначе можно по незнанию включить в них персональные данные и просто чувствительную информацию пользователей, например токены или пароли.
  • Или включить в label-метрики URL запроса и таким образом размножить их в Prometheus. В любом из двух случаев вы получите по шапке от соседних команд или руководства.
3. DSL в бизнес-логике: нееш подумой

Доменно-специфичные языки появились сравнительно давно, один из первых популяризаторов DSL — это язык Groovy. С помощью DSL-конструкций можно элегантно и декларативно решать разные задачи, но есть и подводные камни в виде оверхеда лямбд или сложностей проектирования. И да, если вы пытаетесь лечить бойлерплейт в бизнес-логике приложения с помощью DSL или иных декларативных решений, например data-классами в Python (которые с type hints), дважды подумайте.

Лучше делайте то, что нужно, здесь и сейчас, а не парите в абстракциях. Или же посмотрите в сторону рефакторинга бизнес-кода, если ваш код становится универсальным.
4. Синтаксические возможности языка и их польза для code quality

Как использовать фичи ЯП и не нарываться на ошибки или code smell? Говоря опять про Kotlin, функции-расширения — это очень здорово, мы в Газпромбанке ими активно пользуемся. И идея с композицией вместо наследования в data-классах мне нравится — всё как Джошуа Блох советует.

Но у каждого языка есть и свои сложные синтаксические решения. Inline-методы с noinline- и crossinline-аргументами тому пример. Или ООП в Python, свойства и геттеры-сеттеры. Как и с DSL, если дело дошло до синтаксических приёмов в бизнес-логике, то остановитесь и задумайтесь, как вы оказались в таком положении.

Как можно делать:
Возможные проблемы:
  • Велик соблазн пользоваться языком по полной.
5. ето object. А ето кто?

Речь идёт о null, None, nil, в общем, о нулевых значениях. В принципе, здесь всё просто: не надо, просто не надо писать код с null. Мы решительно не используем null. Только Either, только хардкор от мира ФП! Или sealed classes (проще говоря, классы с приватным конструктором), в конце концов. Но ещё лучше инкапсулировать данные в классы с бизнес-логикой.

Если вам трудно перейти к полному отсутствию null, начните с полумер, другими словами, внедрите null-safety, Optional’ы и null-checking через «?.». Всяко лучше, чем ничего!

Возможный нюанс только один — плохое взаимодействие с внешними библиотеками. Но это решаемо через прослойки и функции-расширения опять же.
6. Гарри Паттерн и кубок кода

Шаблоны проектирования полезны. Мы используем различные паттерны в процессах дизайна кода. Вот лишь часть: ФП, монады, railway-oriented programming (когда код последователен и не перескакивает из места на место). А также мы отказались от исключений. Как? Да просто — через те же sealed classes и Either для описания ошибок как обычных доменных объектов! А не каких-то исключений, которые обрабатываются где-то далеко по стеку.

Вот пример кода с бизнес-логикой, которую можно расширять. И код легко читать.

Смотри какое элегантное решение:
Плюс этого решения с TDD + DDD в том, что он надёжен и прост. Мне, как лиду, легко его контролировать, а разработчику остаётся просто бежать по этому заботливому коридору к ЧистоКоду в конце тоннеля с электроовцами.

Сложности:
  • Трудно перестроить свои мозги под мышление паттернами, думать тем же ФП и монадами.
  • Перевести всю команду на новую «поваренную книгу» - думал будет труднее, но очень много позитивных отзывов. Опытные разработчики повидавшие еще Великих Гигантов отмечают эту историю как элегантной кодописью.
7. Конфиги мои конфиги

Мы в каком-то смысле поклоняемся постулатам The Twelve-Factor App.

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

Другой: не использовать группы настроек, поскольку так можно получить комбинаторный взрыв из сочетаний, — про логику слияния и переопределения параметров я вообще молчу. Поэтому не усложняйте себе жизнь — используйте самое простое решение, environment variables!

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

8. Выбирайте фреймворки правильно. А что там, на заборе, написано?

При открытии какого-то перечня веб-фреймворков из абстрактного awesome-list вам может показаться — вот оно, в этом фреймворке решены все проблемы человечества!

Не обнадёживайте себя и не забывайте, что внедрение такого фреймворка может обернуться адом для всей команды — у неё может быть свой уже сложившийся стиль.

Ни одно решение от этого не застраховано. Да, опенсорс прекрасен, и разные фреймворки по-разному потрясающие! Просто они далеко не всегда решают задачи бизнеса.

Код для машин

9. Делайте тесты. А как ка… тестировать?

Наша команда буквально живёт идеей TDD: мы начинаем код с юнит-тестов, затем пишем интеграционные BDD-тесты бизнес-логики и добавляем иногда щепотку end-to-end-тестов — собственно, всё согласно пирамиде тестирования. Не рожку мороженого — пирамиде!

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

10. CI/CD в помощь команде

Конвейеры автосборки — это просто чудо. С ними можно легко задеплоить сервис на тестовую среду, выполнить тесты и провести проверки кода через SAST или линтинг. Конечно, это не так легко — например, взять те же тесты: среди них могут быть плавающие, флапающие тест-кейсы.

Как бороться: запускайте в CI только небольшой, самый стабильный набор тестов и постепенно расширяйте его по мере развития пайплайна. С SAST надо быть осторожным, поскольку при слишком агрессивных проверках он может не помогать команде, а работать во вред процессам.

Если в целом, то советую начинать CI/CD с автотестов и потом уже внедрять линтеры, анализ кода и анализ зависимостей.
11. Преждевременная оптимизация

Не налегайте слишком на оптимизации в сервисе, ведь вы можете навредить качеству кода, причём сильно. Могу предложить делать оптимизации красиво — так, чтобы это соответствовало гайдлайнам и best practices фреймворка, но для этого нужен навык. При этом некоторые оптимизации можно сделать, даже немного приукрасив кодовую базу: например, паттерны пагинации, batch processing или потоковой обработки могут и оптимизировать код, и сделать его в чём-то понятнее.
Читай эту и другие статьи об IT в Газпромбанке  в нашем блоге на Хабре.
Другие статьи по теме
0%

Банк ГПБ (АО) использует файлы cookie. Подробная информация –
в правилах по обработке персональных данных. Вы можете запретить сохранение cookie в настройках своего браузера.