# #2: Гундосая спека: комменты, Open API

- Выпуск: 2 · Сезон: 1 · Дата: 2022-10-17 · Длительность: 22:21
- Страница: https://apkhmv.xyz/podcast/episode-02/
- Аудио: https://traffic.libsyn.com/secure/173caf0e-b8e8-4056-8a73-2d60d94118ef/02_TF_OpenAPI.mp3
- Ведущий: Александр Пахомов (https://apkhmv.xyz/people/apkhmv/index.md) · Гости: нет
- Темы: git, коммит-месседжи, OpenAPI, REST API, документация
- Расшифровка: draft · Источник: речь участников выпуска, цитируется как есть

## Кратко

Александр Пахомов разбирает, почему коммит-месседж — это история проекта, а не формальность: правила оформления заголовка и тела, conventional commits, атомарные коммиты и главное правило — договориться о формате со всей командой. Во второй части — OpenAPI-спецификация: подходы code-first и design-first, генерация документации и всегда актуального клиента, сервисы, которые продают API без спеки, и история о том, как несовершенный туллинг съел два дня работы из-за стёртых при компиляции имён параметров.

## Главное

- Коммит-месседж должен отвечать на вопросы «что» и «почему»: что поменялось, видно в диффе, а вот почему — только из сообщения коммита.
- Базовые правила: заголовок до 50 символов, с большой буквы, без точки, в imperative mood (add, fix, improve); строки тела до 72 символов; тело отделяется пустой строкой.
- Правило №0 — формат коммитов должен быть единым: о нём надо договориться со всей командой, иначе git log превращается в помойку, по которой невозможно искать.
- Если коммит трудно описать — вы либо сделали лишнее, либо слишком много за раз: это сигнал разбить изменение на более атомарные коммиты.
- Conventional commits добавляют scope и структуру, чтобы поверх git-лога автоматизировать релиз-ноуты и версионирование.
- OpenAPI-спецификация — описание HTTP API, читаемое и человеком, и машиной: из неё генерируются документация и всегда up-to-date клиент.
- Выбор между code-first и design-first — это выбор single source of truth: либо код определяет спецификацию, либо спецификация — код.
- Туллинг несовершенен: Java стирает имена параметров интерфейсов при компиляции (arg0, arg1), из-за чего Micronaut-плагин терял связь @PathVariable с path — два дня на «бинарный поиск» причины.

## Главы

- [00:21](https://apkhmv.xyz/podcast/episode-02/?t=21) Вступление
- [01:10](https://apkhmv.xyz/podcast/episode-02/?t=70) Комментарии к коммитам: пишем историю проекта
- [02:57](https://apkhmv.xyz/podcast/episode-02/?t=177) Правила коммит-месседжа
- [05:25](https://apkhmv.xyz/podcast/episode-02/?t=325) Метаданные, conventional commits, атомарность
- [07:44](https://apkhmv.xyz/podcast/episode-02/?t=464) OpenAPI: что это и зачем
- [09:43](https://apkhmv.xyz/podcast/episode-02/?t=583) Code-first против design-first
- [10:47](https://apkhmv.xyz/podcast/episode-02/?t=647) Single source of truth
- [12:17](https://apkhmv.xyz/podcast/episode-02/?t=737) Кодогенерация, валидация, документация
- [13:56](https://apkhmv.xyz/podcast/episode-02/?t=836) Сервисы без спеки
- [15:08](https://apkhmv.xyz/podcast/episode-02/?t=908) Несовершенный туллинг: история на два дня
- [21:34](https://apkhmv.xyz/podcast/episode-02/?t=1294) Заключение

## Ссылки

- [Conventional Commits — спецификация](https://www.conventionalcommits.org)
- [OpenAPI Initiative](https://www.openapis.org)
- [Подкаст «Запуск завтра»](https://libolibo.ru/zapuskzavtra)

## Похожие выпуски


- [#1: Пилот: слепая печать, коды ошибок](https://apkhmv.xyz/podcast/episode-01/index.md) — общие темы: REST API
- [#9: Прагматичные тулы: plain text, git, shell](https://apkhmv.xyz/podcast/episode-09/index.md) — общие темы: git

## Расшифровка


**[00:21]** Здарова! Меня зовут Саша Пахомов, и я инженер, который любит своё дело. Это второй выпуск подкаста, у которого всё ещё нет ни названия, ни гостей — зато есть куча тем, которые я хочу вместе с вами разобрать. Сегодня мы поговорим о правильном коммит-месседже и о том, почему некоторые люди всё ещё пишут странные комментарии к коммитам. А во второй части продолжим тему REST API и погрузимся в OpenAPI-спецификацию: стоит ли генерировать спецификацию из кода или, наоборот, код из спецификации, какие инструменты даёт Java-стек и почему в 2022 году отсутствие OpenAPI-спеки — не красный, но точно жёлтый флажочек. Поехали!

**[01:10]** Давно ли вы просматривали git log? Если в командной строке это делают редко, то в IDE — обыденность. А git blame — когда мы хотим узнать автора конкретной строчки кода? Я это делаю постоянно. И вот что меня дико раздражает, так это комментарии к коммитам вида «fixed tests», «refactoring», «algorithm» или, вот любимое, «minor update». Какую информацию дают эти заголовки? Полезной — совсем никакой. Вероятно, автор очень спешил, или обладал слабым знанием английского, или попросту был некомпетентен. Не пишите подобные комментарии. Окей, а какие тогда надо — сейчас обсудим.

**[01:54]** Вообще говоря, проект определяется не только текущим снапшотом кодовой базы, но и историей её развития. Поясню: проект существует не только здесь и сейчас, но и во времени — это процессы, принятые решения, проведённые исследования. С задачей хранения снапшота Git справляется отлично — для этого он, казалось бы, и создан. А вот хранить в Git историю мало кто считает чем-то естественным: для неё заводят проектную документацию или что-то ещё. Я же всегда пишу комментарии, исходя из того, что пишу историю развития кода: почему я сделал так, а не иначе, зачем здесь стоит этот коммент, почему я оставил ссылку на исследование.

**[02:57]** Так как же писать комментарии к коммиту? Существуют некоторые правила (в кавычках), которых следует (опять же в кавычках) придерживаться. Первое: заголовок пишем с большой буквы и без точки в конце. Второе: imperative mood — не «edited» в прошедшем времени и не длительное «refactoring», а add, create, improve — будто мы формулируем запрос «I would like to…». Третье: заголовок ограничен 50 символами, и это строгое ограничение, а строка тела — 72 символами. Тело — это всё, что не заголовок; заголовок — первая строчка. И четвёртое: заголовок отделяется от тела пустой строкой.

**[03:57]** Но, с моей точки зрения, самое главное правило должно идти под номером ноль: о формате коммит-месседжей нужно договориться со всеми — они должны быть единообразными. Когда один коммит написан в одном стиле, другой в другом, а третий вообще как попало — это, во-первых, раздражает, а во-вторых, такой лог невозможно грепать, в нём нельзя искать и группировать. Он превращается просто в помойку. При этом всё перечисленное — не жёсткие догмы: строгого «должно быть только так» нет.

**[04:37]** Сам коммит-месседж должен стараться ответить на вопросы «что» и «почему». Что меняет это изменение — какую часть продукта, какое поведение — обычными человеческими словами. И почему я сделал так, а не иначе: почему удалил эту строчку, хотя она казалась важной, почему добавил вот этот тест-кейс. Рассказать нужно историю. Причём на вопрос «что» отвечает и сам код: открой коммит и посмотри, что поменялось — вот класс, вот строка. А вот почему это было поменяно — в диффе не видно совершенно. Это и должно быть в body коммит-месседжа.

**[05:25]** Помимо текстового описания, в коммит-месседж стоит добавлять метаданные: номер тикета в джире, ссылку на тикет в конце или так называемый scope. Про scope стоит сказать отдельно — он определяется в conventional commits. Это люди пошли дальше: им мало человекочитаемых заголовка и тела — хочется накрутить поверх git-лога автоматизацию: генерировать из коммитов релиз-ноуты, формировать minor- и major-версии продукта. Для этого conventional commits и придумали. Не думаю, что это топик для дискуссии в подкасте, но ссылку оставлю. И ещё одна мысль: если вы садитесь описывать коммит и сидите в ступоре — «а что я, собственно, сделал?» — есть два варианта. Либо вы сделали то, чего делать не надо было, либо пытаетесь сделать слишком много изменений за раз. Это звоночек: возможно, стоит пересмотреть изменение и сделать его более атомарным. Лучше менять одну часть функциональности в одном коммите, чем засовывать в него кучу фичей и багфиксов разом, — это антипаттерн.

**[07:02]** И последняя мысль на этот счёт. Есть множество статей, утверждающих: если человек не читает git log, он будет писать непонятные комментарии. Я считаю, причина не в этом — людям просто лень. Это банальное отношение к своей работе: тебе всё равно, что будут читать те, кто выполнит git blame по твоим изменениям, — нужно просто закоммитить, заревьюить, залить, закрыть таску в джире и пойти пить свой кофе. Лениться не стоит: пишите развёрнутые git-комментарии — вам потом спасибо скажут.

**[07:44]** А теперь к основной теме подкаста. В начале карьеры мне попадались задачи на интеграцию со сторонними HTTP-сервисами. Документация у них была вида: вот endpoint по такому-то пути, такие хедеры, такое body возвращаем — вот, пожалуйста. Я читал всю документацию, открывал Postman, тыкал в API, потом шёл в проект и неделю, а то и две, потел над REST-клиентом, тестировал его, проходил ревью, чинил баги. Кто бы мне тогда сказал, что вся эта работа уже автоматизирована и мне платят деньги ни за что! Будь у того сервиса OpenAPI-спецификация, работа на две недели заняла бы от силы пару дней. Давайте разберёмся, что это за спецификация и как она может помочь.

**[08:35]** OpenAPI-спецификация определяет стандарт, который описывает интерфейс HTTP API в виде, доступном и человеку — в человекочитаемом формате, — и машине: машина может, например, сгенерировать клиент к этому API или провалидировать его. На приземлённом уровне это просто файлик в формате JSON или YAML, в котором описаны эндпоинты, дескрипшены к ним, хедеры запросов, возвращаемые коды (если про коды интересно подробнее — послушайте предыдущий выпуск), security и так далее. Всё, что нужно знать про HTTP API, описано в одном файле в специальном формате — компромиссе между человеком и машиной. Собственно, для этого в своё время изобрели и языки программирования.

**[09:43]** Существует два глобальных подхода к разработке с помощью OpenAPI-спецификации. Первый — code-first: мы пишем код бэкенда — контроллер, спринговый или любой другой, — комментарии к нему, его методам и параметрам, возможно, добавляем аннотации (чаще всего из библиотеки Swagger Annotations), прикручиваем плагин генерации — Gradle-плагин, Maven-плагин или CLI-тулзу — и после сборки получаем тот самый файл OpenAPI-спецификации, где всё описано. Второй подход — design-first: сначала пишем спецификацию, согласуем её с аналитиком, с product owner'ом — зависит от специфики работы, — а уже из неё генерируем интерфейсы контроллеров для бэкенда.

**[10:47]** Какой подход выбрать, зависит от решаемой задачи. Важно понимать одну простую вещь: выбирая подход, вы определяете единственный источник правды — single source of truth. Если у нас design-first и мы сначала пишем спецификацию, то источник правды — она: спецификация определяет, как будет себя вести сервис и какие в нём будут эндпоинты. Если code-first — источником правды является код: как он работает, так и есть, а OpenAPI-спецификация — производная от кода, отражающая то, что он делает. Если у вас в команде есть аналитик, тестировщик, продукт, и вы хотите обсуждать спецификацию с ними — скорее всего, стоит использовать design-first: они откроют её в каком-нибудь Swagger-эдиторе, сразу увидят API, потыкают и дадут комментарии ещё до того, как вы её заимплементировали. А если, как в моём случае с разработкой базы данных, источник правды — код и нужно «слушать код», то спецификация — производная, nice-to-have вещь: есть — супер, нет — в целом мир не рушится.

**[12:17]** Как я уже говорил, существует кодогенерация спецификации из кода и кода из спецификации — в зависимости от подхода. Это инструмент быть уверенным, что спека соответствует коду, а код — спеке: что параметр name с маленькой буквы и типа string из спецификации действительно есть в коде. Иначе возьмёшь спецификацию, сгенерируешь по ней клиента — а работать он не будет. Есть и набор валидаторов, которые берут на вход спецификацию и код, анализируют их — статически и не только — и говорят, соответствуют они друг другу или нет. Отличный кандидат для CI-пайплайна.

**[13:02]** Прелесть OpenAPI-спецификации в том, что из неё можно сгенерировать документацию — в целом неплохую статическую документацию, которую можно просто читать на сайте, — и сгенерировать клиента. Тот мой случай из начала — чтение сайта, выяснение краевых случаев, ручное написание клиента — всё это могло быть заменено генерацией клиента из спецификации. Это вообще киллер-фича: я беру спеку, генерирую из неё клиента один раз — или перегенерирую в CI-пайплайне каждый раз — и всегда получаю up-to-date код, который просто беру и использую. И я знаю, что он работает: если параметр name обязательный, клиент скажет — «чувак, пожалуйста, положи параметр name, без этого я компилироваться не буду». Супер вещь.

**[13:56]** Но несмотря на широкое распространение OpenAPI-спецификации, некоторые сервисы, цель которых — предоставить API и лёгкую интеграцию с ним, всё ещё её не дают. То есть люди зарабатывают деньги на предоставлении API, а спеки нет. У меня от этого дико бомбит. Пример — ЮMoney, конкретно их эквайринг: документация на сайте абсолютно замечательная, божественная, в целом всё понятно — но OpenAPI-спецификации нет. «Мы предоставляем API как инструмент, с нами легко интегрироваться» — а спеки нет. Сорян. Это ещё ладно — там хотя бы документация отличная, за вечер можно разобраться. А вот у Тинькофф в том же эквайринге, помимо того что спецификации, конечно, нет, ещё и документация отвратительная: пойди просто разберись, как с этим работать. И таких примеров полно. Хотя OpenAPI — уже де-факто стандарт, и спецификации сейчас разрабатываются вместе с сервисом практически сразу, как юнит-тесты, многие сервисы их не предоставляют.

**[15:08]** Как у любой технологии, у подхода разработки через спеку есть минусы — это несовершенный туллинг. И о нём у меня есть история. Эту часть подкаста я начал с рассказа о том, как OpenAPI-спецификация могла бы сэкономить мне неделю работы, — а теперь расскажу, как она потратила два или три дня впустую. У меня есть Micronaut-бэкенд, контроллеры которого аннотированы Swagger Annotations; на стадии билда Maven-плагин Micronaut OpenAPI генерирует из них спецификацию, а в другом модуле из этой спецификации генерируется клиент для CLI-тулзы. Стандартный code-first. Контроллеры лежали в одном модуле, и в нём же был определён плагин кодогенерации. В один прекрасный момент я решил: отчего это контроллеры и имплементации лежат вместе — надо бы сделать decoupling и вынести интерфейсы в отдельный модуль. Переименовал классы контроллеров в интерфейсы, убрал поля и тела методов, вынес в отдельный Maven-модуль. Запускаю ту же кодогенерацию в билде — ошибка: «cannot compile, Maven plugin compilation error». Совершенно неадекватная ошибка. Погуглил — естественно, ничего. Добавил stack trace, -X и все возможные дебаг-флаги Maven-сборки — и не получил вообще ничего: «path variable not found», «Maven compilation error» — всё.

**[17:02]** Как быть, что делать? Тут я решил применить подход, который называю бинарным поиском проблемы в программировании, — моё авторское название вполне понятного всем метода, когда мы локализуем проблему, уменьшая количество входных данных. У меня есть чёрный ящик — несобирающаяся Maven-сборка. Есть входные данные — мой исходный код и тот факт, что классом в старом модуле оно собиралось, а интерфейсом в другом — нет. Я хочу уменьшить входные данные до минимально возможных и получить ту же проблему — тем самым локализовав её причину. Думаю: ладно, я переименовал класс в интерфейс — давай обратно: интерфейс в класс в том же модуле. Запускаю — работает. Ага, дальше. Возвращаю интерфейс — не работает. Начинаю по одному убирать определения методов: убрал первый — не работает, второй — не работает, третий — заработало. Ага: значит, дело не в ключевом слове class или interface, а в конкретном методе внутри интерфейса. Таким путём я локализовал проблему до стечения трёх обстоятельств: это интерфейс; он лежит в модуле, дочернем к тому, где генерируется спека; и в нём есть аннотация @PathVariable. Убираем любое из трёх — сборка работает.

**[18:45]** Потратив на этот поиск два дня — процесс реально долгий — плюс почитав сорцы Micronaut OpenAPI-плагина, я пришёл к решению; само оно оказалось тривиальным, но техническая суть была вот в чём. Сейчас будет немного сложно для подкаста, но я попытаюсь объяснить, потому что это реально дичь. За что отвечает аннотация @PathVariable? Мы аннотируем ею входной параметр метода и говорим: подставь сюда значение из path. Допустим, есть `/users/1`, где 1 — это id. В определении контроллера мы пишем путь `/users/{id}`, называем параметр метода тоже id, аннотируем его @PathVariable — и фреймворк подставит ту самую единичку в параметр: он видит, что имя параметра id и в пути тоже id, два плюс два равно четыре. И это работает. До поры до времени.

**[19:45]** До какой же поры? До тех пор, пока имя параметра сохраняется в compile time. Если мы компилируем класс, имена параметров методов в class-файле, как правило, сохраняются (либо можно передать компилятору специальные ключи). Открываем Java-класс с `public static void main(String[] args)`, смотрим в скомпилированный class-файл — переменная называется args, как в сорцах. Но если скомпилировать интерфейс, оригинальные имена параметров стираются: в class-файле они называются var1, var2 либо arg0, arg1, arg2. Java просто берёт и стирает имена при компиляции. Это known issue, и для людей, давно работающих с Java и знающих, как она компилируется, — не новость. Но для разработчиков плагина это, видимо, в какой-то момент было новостью — и они это не учли. Что получается: есть интерфейс с методом, параметр которого должен подставиться в path, и аннотация @PathVariable; когда мы компилируем этот интерфейс в другом модуле, имена параметров стираются — и Micronaut-плагин, потребляя class-файл, уже не может сопоставить @PathVariable и arg0, которого в HTTP path нет. Надеюсь, проблему я немного объяснил. Суть, впрочем, не в том, чтобы понять, почему так произошло, а в том, чтобы иметь в виду: туллинг реально несовершенен, и это несовершенство иногда приводит к тому, что проект просто не компилируется и ты не понимаешь, что происходит, — хотя изменение с точки зрения программирования было тривиальнейшее. Проблему я решил, но потратил на это два дня.

**[21:34]** Ну и в заключительной части хочу посоветовать подкаст. Называется он «Запуск завтра»: ведущий разбирает технические вопросы с гостем-экспертом на уровне, который поймёт даже человек, далёкий от IT. Отличный вариант, чтобы разбавить технические дебри моего подкаста. Ссылка будет в описании. Ну а на этом всё. Услышимся!


