Недавно мы перезапустили API Яндекс.Кассы – платежного сервиса с 15-летней историей. Я хочу рассказать, как решить такую амбициозную задачу. Материала набралось на серию статей, поэтому здесь я подробно расскажу о проектировании, переработке наших API, а также про наши инструменты и процессы.
Ключевые слова для оценки полезности: API, REST, OpenAPI, Swagger, рефакторинг взаимодействия систем.
Постановка проблемы, Или о чем этот разговор
Когда маленький коллектив превращается из стартапа в большую компанию, весь объем знаний о программных компонентах и их взаимодействии в уме не удержишь. Отсюда две сложности:
- У разных подразделений свои планы, и не всегда можно делать работу одновременно. Договариваться о порядке взаимодействия и документировать его приходится на берегу.
- Важно сохранить эталонное знание о правилах работы сервиса и интеграции с ним. Это гораздо сложнее, если не зафиксировать порядок взаимодействия сторон.
Методологии разработки вроде Scrum позволяют ненадолго снять эти проблемы:
- Сложность некоторых задач выходит за рамки одной команды и их нужно решать сообща.
- Есть задачи, которые нельзя однозначно отнести только к одному из продуктов, они требуют согласованных изменений в нескольких компонентах системы.
- Задачи сопровождения требуют доработки старых сервисов. Настолько старых, что знания о них потеряли былую яркость.
За десять лет разработчики Яндекс.Денег убедились в этом, пройдя путь описания взаимодействий от Wiki-страниц, через XML schema, JSON schema до OpenAPI/Swagger.
Инструменты проектирования спецификаций API – вначале было слово
Все началось с описания порядка взаимодействия сервисов в Wiki и Microsoft Word, с примерами запросов и ответов. Для передачи данных, как правило, использовался XML. Это уже было лучше чем ничего, но такой способ документирования годится только для передачи знаний от человека к человеку.
Произвольное текстовое описание может неоднозначно интерпретироваться людьми из разных подразделений, поэтому потребовалась единая система понятий и терминов. По мере роста количества разработчиков стала необходимой и формализация описаний взаимодействий.
Формальное описание операций, типов данных и их граничных условий нужно не только для разработки, но и для автоматизации модульного тестирования. Первые формальные контракты API-сервисов мы описывали в формате XML schema, позже пробовали и JSON schema. Но оба не идеальны, что мешает полностью перейти на них с текстовых описаний Wiki или Microsoft Word:
- В XML schema невозможно добавить полноценную документацию с описанием сценариев работы, ее приходится вести отдельно.
- JSON schema – слишком сырая спецификация без достаточной инструментальной поддержки, тоже не позволяет включать полноценную документацию.
Нам идеально подошел OpenAPI, ранее известный как Swagger. OpenAPI Initiative это открытый проект под управлением Linux Foundation. Я убежден, что его ждет большое будущее.
OpenAPI 3 позволяет в одном файле спецификации объединить документацию и описание формата взаимодействия. Это очень важное качество — у вас никогда не будет рассинхронизации текстовой документации и файла спецификации API.
В наших проектах используются OpenAPI 3 файлы спецификаций в формате YAML, и вот почему:
- Человеку YAML читать удобнее, чем JSON.
- Он включает документацию в формате CommonMark (в прошлом Markdown). Это форматирование, списки, таблицы, цитаты, примеры кода.
- Документация добавляется к нужному объекту спецификации, а не пишется в отдельных разделах.
Декомпозиция сервисов и управление изменениями
Типовая проблема, с которой сталкиваются многие организации – управление изменениями документов. Случается так, что параллельно существует множество документов с отдельными изменениями для разных проектов. Поддерживать документацию для множества проектов очень тяжело, поэтому высока вероятность человеческих ошибок.
Спецификация OpenAPI – это текстовый файл в формате YAML, а значит, с ним можно работать как с кодом:
- Использовать системы контроля версий.
- Гибко управлять изменениями на основе ветвей, тэгов, релизов.
- Параллельно делать изменения для множества проектов множеством людей.
- Построить процедуры ревью и приемки изменений в основной документ.
- Ссылаться на конкретные версии документа в контексте определенного проекта.
В наших проектах используется система контроля версий Atlassian Bitbucket c плагином Web Pages. Это позволяет одновременно работать со спецификацией, как с кодом, и видеть собранную документацию в HTML-формате.
При декомпозиции какой-либо крупной задачи на набор сервисов рекомендую опираться на принцип разделения функциональности по доменам прикладных областей, а не по технологической общности.
За каждым сервисом API у нас стоит определенный бизнес-процесс, и каждый сервис API описывается отдельным файлом OpenAPI-спецификации. А за него отвечает своя продуктовая команда. С учетом этого, в наборе файлов спецификаций OpenAPI каждый файл соответствует своему прикладному продукту.
Поэтому остальное было делом техники: осталось завести соответствующие репозитории в Bitbucket и настроить группы ревьюеров, ответственных за каждый сервис API. В результате в Bitbucket у нас появился проект для API-спецификаций, представляющий собой общий каталог наших API.
Файлы спецификаций сгруппированы по целевым продуктам:
- API-спецификации одного продукта размещаются в одном репозитории.
- API разных продуктов размещаются в разных репозиториях.
Иными словами, один репозиторий соответствует одному продукту – команде, ответственной за процессы развития и сопровождения бизнес-процессов продукта и спецификаций его API.
По результатам множества выполненных проектов, структура Git-репозитория стала следующей:
- Ветвь master отражает состояние актуальной спецификации, которая де-факто находится на боевой системе и доступна для использования.
- Ветвь prototype предназначена для эскизных проектов, для проработки набора сценариев использования продукта. В дальнейшем может быть полезна для ретроспективы начальных построений, которые легли в основу последующих технических решений.
- Проектные задачи разрабатываются в feature-ветвях. Одна feature-ветвь представляет собой один проект или задачу по модификации API.
- Проект содержит файл index.html с настройками инициализации Swagger-UI. Благодаря плагину Web Pages это позволяет отображать документацию в виде HTML. Таким образом, каждая ветвь репозитория отображает HTML-документацию онлайн, и на нее можно ссылаться из внешних систем и документов.
Кроме структуры, пришлось разработать и правила работы с репозиторием:
- Прав на запись в master- и prototype-ветви нет ни у кого, прямая запись запрещена. Все изменения спецификации оформляются как Pull Requests из feature-ветвей.
- Чтобы изменения из Pull-Request попали в основную спецификацию, этот Pull-Request должен получить Approve от всех обязательных ревьюеров и от любого количества необязательных ревьюеров.
- Для обсуждения и согласования изменений отлично подходят Pull-Requests, история которых представляет собой историю задач, вносящих изменения в спецификацию.
- Возможно внесение любых изменений в feature-ветвь на любом этапе существования Pull-Request, однако новые изменения потребуют повторного согласования с ревьюверами.
- Слияние Pull-Request в master-ветвь осуществляется при выпуске рабочего релиза в боевую эксплуатацию.
Благодаря этим принципам мы получили удобную, прозрачную и предсказуемую среду работы со спецификациями.
Подход Design-first как основа качественного решения задачи
При проектировании новых сервисов мы опираемся на принципы REST, но не соблюдаем их в полной мере – например, если это усложняет архитектуру сервиса или противоречит здравому смыслу.
Ценность REST в том, что этот подход обязывает произвести декомпозицию сервиса на набор сущностей и действий с ними.
REST – удобное отражение подхода к проектированию Domain Driven Design. На мой взгляд, благодаря REST-архитектуре у нас получаются более простые и качественные объектные модели прикладных задач, если сравнивать с тем, что мы ранее делали при помощи RPC-подходов.
Чтобы создать качественный продукт, разработчику требуется тщательно изучить предметную область задачи вне контекста опыта предыдущих проектов. Поэтому Design-first хорошо зарекомендовал себя как подход решения прикладных задач. Его можно разбить на два этапа:
Изучение, описание, а также формализация сущностей и процессов предметной области.
- Формализация сценариев использования вашего API-сервиса с учетом возможных связей с другими сервисами. Используйте термины предметной области, а не внутренний технический жаргон — так вы избежите неверного понимания работы вашего сервиса со стороны его пользователей.
Результатом этой работы будет спецификация API сервиса, которая:
- Определяет набор сущностей предметной области и их атрибуты.
- Определяет жизненный цикл и набор состояний каждой сущности.
- Определяет набор действий с каждой сущностью и возможные ошибочные ситуации
Я бы не рекомендовал подход Code-first, когда спецификация API генерируется из существующей реализации: она всегда наследует решения из предыдущих проектов, а не решает задачу. Не существует известных методов решить парадокс курицы и яйца – для создания качественной реализации сперва следует изучить предметную область и провести проектирование сервиса, невозможно создать требуемую реализацию раньше, чем проведено исследование задачи и проектирование.
Антипаттерны REST
Вдохновившись статьей «REST — это новый SOAP», я хотел бы поделиться практическими соображениями по теме примеров некорректного применения REST – ведь это не серебряная пуля и, надеюсь, не золотой молоток в ваших руках.
REST это не только CRUD
Просто удивительно выглядит стремление коллег по цеху упаковать все процессы в модель Create-Read-Update-Delete. Жизнь сложнее и богаче: бизнес-процессы могут состоять из множества операций, сущности могут иметь множество состояний и переходов между ними.
Многие статьи о REST ссылаются на работу Roy Thomas Fielding «Architectural Styles and the Design of Network-based Software Architectures» как первоисточник определения REST (смотрите пятую и шестую главы этой работы). Рекомендаций использовать http- глаголы GET-POST-PUT-DELETE как единственный способ определения операций вы там не найдете.
REST – это принцип декомпозиции сервисов на набор ресурсов и операций над ними. Если вы реализуете всю требуемую функциональность на базе только POST запросов, то это тоже будет REST.
Отражение ошибок бизнес-логики на основе HTTP status
Помните о том, что HTTP-протокол осуществляет транспортную функцию по доставке данных в запросах и ответах. Не следует смешивать уровни бизнес-логики и передачи данных. Четко разделяйте понятия «http-запрос» и «действие бизнес-процесса». HTTP-status коды предназначены для отражения состояния выполнения запроса HTTP, их не следует использовать для задач уровня бизнес-логики.
Тем не менее, мы часто используем сопутствующие протоколы HTTP, которые накладывают свои обязательства на формат ответа, например:
Вот некоторые типовые ситуации, в которых уместно использовать HTTP status:
- 400: Неверный формат запроса, формат аргументов запроса.
- 401: Отсутствует аутентификация клиента.
- 403: Недостаточна авторизация клиента.
- 5xx: Техническая ошибка выполнения запроса, недоступность сервиса или промежуточного шлюза.
Отказы уровня бизнес-логики следует отражать как атрибуты сущности, над которой выполнялась операция этим HTTP-запросом.
К примеру, отказ в проведении платежа может быть обусловлен недостатком средств на счете клиента. При этом все http-запросы для проведения платежа будут выполнены успешно. Такую ситуацию следует отражать в виде атрибутов состояния сущности «Платеж».
Использование PUT/PATCH-запросов для операций бизнес-логики
PUT-запросы не всегда подходят для операций бизнес-логики, так как они предназначены для замещения документа на сервере новым документом того же типа и структуры. GET-запрос к тому же ресурсу должен возвращать аргумент PUT-запроса.
Стандарт определяет PUT-запрос как:
The PUT method requests that the state of the target resource be сreated or replaced with the state defined by the representation enclosed in the request message payload.
RFC 7231 sec 4.3.4
Пример корректного применения PUT-запроса — загрузка файла на Яндекс.Диск.
RFC 5789 PATCH-запрос тоже ограничен в применимости: его семантика аналогична PUT, тело запроса представляет собой RFC 6902 JSON patch документ, а не любой документ вообще.
Пример JSON patch документа:
[
{
"op": "remove",
"path": "/a/b/c"
},
{
"op": "add",
"path": "/a/b/c",
"value": [
"foo",
"bar"
]
},
{
"op": "replace",
"path": "/a/b/c",
"value": 42
},
{
"op": "move",
"from": "/a/b/c",
"path": "/a/b/d"
}
]
Согласитесь, описать операции бизнес-логики простым и доступным способом с таким синтаксисом будет непросто.
Итак, если ваша задача удовлетворяет выше указанным требованиям, используйте PUT/PATCH, в противном случае лучше применять POST.
Пару слов в заключение
ИТ-системы непрерывно развиваются и усложняются. Пожалуй, главным вызовом сегодняшнего дня является ограничение роста сложности систем. По моему мнению, методы правильной декомпозиции систем и управления изменениями это хорошее подспорье в нашем нелегком труде.
Надеюсь, представленный материал был вам полезен. Смотрите также мой доклад о проектировании REST-like API на JavaJam, вот ссылка на запись.