You've already forked The-API-Book
mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-08-10 21:51:42 +02:00
major refactoring - additions to existing chapters
This commit is contained in:
36
src/ru/drafts/01-Введение/03.md
Normal file
36
src/ru/drafts/01-Введение/03.md
Normal file
@@ -0,0 +1,36 @@
|
||||
### Обзор существующих решений в области разработки API
|
||||
|
||||
Цель первых трёх разделов настоящей книги — поговорить о дизайне API вне привязки к какой-либо конкретной технологии. Изложенные нами принципы равно применимы как к веб-сервисам, так и, например, к интерфейсам операционных систем.
|
||||
|
||||
Тем не менее, два больших сценария использования явно доминируют, когда мы говорим о разработке API:
|
||||
* разработка клиент-серверных приложений;
|
||||
* разработка клиентских SDK.
|
||||
|
||||
В первом случае мы говорим практически исключительно об API, работающих поверх протокола HTTP. В настоящий момент клиент-серверное взаимодействие, не опирающееся на HTTP, можно встретить разве что в протоколе WebSocket (хотя и он может быть использован совместно с HTTP, что часто и происходит), а также в различных форматах потокового вещания и других узкоспециализированных интерфейсах.
|
||||
|
||||
#### HTTP API
|
||||
|
||||
Несмотря на кажущуюся гомогенность технологии ввиду использования одного протокола прикладного уровня, в действительности же наблюдается значительное разнообразие подходов к имплементации HTTP API.
|
||||
|
||||
**Во-первых**, существующие протоколы различаются подходом к утилизации собственно протокола HTTP:
|
||||
* либо клиент-серверное взаимодействие опирается на описанные в стандарте HTTP возможности (точнее было бы сказать, «стандартах HTTP», поскольку функциональность протокола описана во множестве разных RFC);
|
||||
* либо HTTP утилизируется как транспорт, и поверх него выстроен дополнительный уровень абстракции (т.е. возможности HTTP, такие как номенклатура ошибок или заголовков, сознательно редуцируются до минимального уровня, а вся мета-информация переносится на уровень вышестоящего протокола).
|
||||
|
||||
К первой категории относятся API, которые принято обозначать словом «REST» или «RESTful». а ко второй — все виды RPC, а также прикладные протоколы типа FTP или SSH.
|
||||
|
||||
Во-вторых, реализации HTTP API опираются на разные форматы передаваемых данных:
|
||||
* REST API и некоторые RPC (JSON-RPC, GraphQL) полагаются в основном на формат JSON (опционально дополненный передачей бинарных файлов);
|
||||
* GRPC, а также Apache Avro и другие специализированные протоколы полагаются на бинарные форматы (такие как Protocol Buffers или FlattBuffers);
|
||||
* наконец, некоторые RPC-протоколы (SOAP, XML-RPC) используют для передачи данных формат XML (что на сегодня является скорее устаревшей технологией).
|
||||
|
||||
Все перечисленные технологии оперируют существенно разными парадигмами — и вызывают естественным образом большое количество холиваров — хотя на момент написания этой книги можно констатировать, что для API общего назначения выбор практически сводится к триаде «REST API (фактически, JSON over HTTP) против GRPC против GraphQL».
|
||||
|
||||
HTTP API будет посвящён раздел IV; мы также отдельно и подробно рассмотрим концепцию «REST API», поскольку, в отличие от GRPC и GraphQL, она является более гибкой и низкоуровневой — но и приводит к большему непониманию по той же причине.
|
||||
|
||||
#### SDK
|
||||
|
||||
Понятие SDK, вообще говоря, вовсе не относится к API: это просто термин для некоторого набора программных инструментов. Однако, как и за «REST», за ним закрепилось некоторое определённое толкование — как клиентского фреймворка для работы с некоторым API. Это может быть как обёртка над клиент-серверным API, так и UI-библиотека в рамках какой-то платформы. Существенным отличием от вышеперечисленных API является то, что «SDK» реализован для какого-то конкретного языка программирования, и его целью является как раз превращение абстрактного набора методов (клиент-серверного API или API операционной системы) в конкретные структуры, разработанные для конкретного языка программирования и конкретной платформы.
|
||||
|
||||
В отличие от клиент-серверных API, обобщить такие SDK не представляется возможным, т.к. каждый из них написан под конкретное сочетание язык программирования-платформа. Из интероперабельных технологий в мире SDK можно привести в пример разве что React / React Native.
|
||||
|
||||
Тем не менее, SDK обладают общностью *на уровне задач*, которые они решают (см. выше), и именно этому (решению проблем трансляции и предоставления UI-компонент) будет посвящён раздел V настоящей книги.
|
19
src/ru/drafts/01-Введение/05.md
Normal file
19
src/ru/drafts/01-Введение/05.md
Normal file
@@ -0,0 +1,19 @@
|
||||
### API-first подход
|
||||
|
||||
На сегодняшний день всё больше и больше IT-компаний понимают и принимают важность концепции «API-first», т.е. парадигмы разработки ПО, в которой главной составляющей является разработка API.
|
||||
|
||||
Следует, однако, различать API-first подход в продуктовом и техническом смысле. Первое означает, что при разработке некоторого сервиса сначала как первый (и иногда единственный) шаг разрабатывается API к нему, и мы обсудим этот подход в разделе «API как продукт».
|
||||
|
||||
Если же мы говорим об API-first подходе в техническом смысле, то мы имеем в виду следующее: **контракт, т.е. обязательство связывать программные контексты, предшествует реализации и определяет её**. Конкретнее, речь идёт о двух принципах:
|
||||
* контракт разрабатывается и фиксируется в виде спецификации до того, как функциональность непосредственно реализована;
|
||||
* если обнаруживается несоответствие контракта и его имплементации, изменения вносятся в имплементацию, а не в контракт.
|
||||
|
||||
Здесь под спецификацией мы понимаем формальное машиночитаемое описание контракта на одном из языков определения интерфейсов (Interface Definition Language, IDL) — например, в виде Swagger/OpenAPI спецификации или .proto-файла.
|
||||
|
||||
Оба вышеуказанных принципа фактически утверждают приоритет интересов партнёра-разработчика API:
|
||||
* первый принцип позволяет партнёру разрабатывать код по формальной спецификации, не требуя согласования с провайдером API;
|
||||
* появляется возможность использовать генерацию кода по спецификации, что может существенно упрощать и автоматизировать разработку;
|
||||
* партнёрский код может быть написан вообще в отсутствие доступа к API;
|
||||
* второй принцип позволяет не требовать изменений в коде партнёра, если обнаружен ошибка в реализации API.
|
||||
|
||||
Таким образом, API-first подход — это некоторая гарантия для ваших потребителей. Но, как легко заметить, работает эта гарантия только в условиях качественного дизайна API: если в фазе разработки спецификации были допущены неисправимые ошибки, то второй принцип придётся нарушить.
|
@@ -87,7 +87,7 @@ orders.calculateAggregatedStats({
|
||||
|
||||
Отдельное следствие из этого правила — денежные величины *всегда* должны сопровождаться указанием кода валюты.
|
||||
|
||||
Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что, как ни сделай, — кто-то останется недовольным. Классический пример такого рода — порядок географических координат («широта-долгота» против «долгота-широта»). Здесь, увы, есть только один работающий метод борьбы с фрустрацией — «блокнот душевного спокойствия», который будет описан в разделе II.
|
||||
Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что, как ни сделай, — кто-то останется недовольным. Классический пример такого рода — порядок географических координат («широта-долгота» против «долгота-широта»). Здесь, увы, есть только один работающий метод борьбы с фрустрацией — «блокнот душевного спокойствия», который будет описан в разделе «Обратная совместимость».
|
||||
|
||||
##### Сущности должны именоваться конкретно
|
||||
|
||||
@@ -219,6 +219,14 @@ GET /coffee-machines/{id}/stocks
|
||||
```
|
||||
— то разработчику потребуется вычислить флаг `!beans_absence && !cup_absence`, что эквивалентно `!(beans_absence || cup_absence)`, а вот в этом переходе ошибиться очень легко, и избегание двойных отрицаний помогает слабо. Здесь, к сожалению, есть только общий совет «избегайте ситуаций, когда разработчику нужно вычислять такие флаги».
|
||||
|
||||
##### Декларируйте технические ограничения явно
|
||||
|
||||
У любого поля в вашем API есть ограничения на допустимые значения: максимальная длина текста, объём прикладываемых документов в мегабайтах, разрешённые диапазоны цифровых значений. Часто разработчики API пренебрегают указанием этих лимитов — либо потому, что считают их очевидными, либо потому, что попросту не знают их сами. Это, разумеется, один большой антипаттерн: незнание пределов использования системы автоматически означает, что код партнёров может в любой момент перестать работать по не зависящим от них причинам.
|
||||
|
||||
Поэтому, во-первых, указывайте границы допустимых значений для всех без исключения полей в API, и, во-вторых, если эти границы нарушены, генерируйте машиночитаемую ошибку с описанием, какое ограничение на какое поле было нарушено.
|
||||
|
||||
То же соображение применимо и к квотам: партнёры должны иметь доступ к информации о том, какую долю доступных ресурсов они выбрали, и ошибки в случае превышения квоты должны быть информативными.
|
||||
|
||||
##### Отсутствие результата — тоже результат
|
||||
|
||||
Если сервер корректно обработал вопрос и никакой внештатной ситуации не возникло — следовательно, это не ошибка. К сожалению, весьма распространён антипаттерн, когда отсутствие результата считается ошибкой.
|
||||
@@ -254,39 +262,88 @@ POST /v1/coffee-machines/search
|
||||
|
||||
Это правило вообще можно упростить до следующего: если результатом операции является массив данных, то пустота этого массива — не ошибка, а штатный ответ. (Если, конечно, он допустим по смыслу; пустой массив координат, например, является ошибкой.)
|
||||
|
||||
##### Валидируйте корректность операции
|
||||
**NB**: этот паттерн следует применять и в обратную сторону: если в запросе можно указать массив сущностей, то следует отличать пустой массив от отсутствия параметра. Рассмотрим следующий пример:
|
||||
|
||||
В ситуации выбора: указать на ошибку или молча её проглотить — разработчик API должен всегда выбирать первый вариант.
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
POST /v1/coffee-machines/search
|
||||
// Находит все рецепты кофе
|
||||
// без молока
|
||||
POST /v1/recipes/search
|
||||
{
|
||||
"recipes": ["lngo"]
|
||||
// Положение пользователя не задано
|
||||
"filter": {
|
||||
"no_milk": true
|
||||
}
|
||||
}
|
||||
→
|
||||
→ 200 OK
|
||||
{
|
||||
"results": [
|
||||
// Результаты для какой-то
|
||||
// локации по умолчанию
|
||||
"results": [{
|
||||
"recipe": "espresso"
|
||||
…
|
||||
}, {
|
||||
"recipe": "lungo",
|
||||
…
|
||||
}]
|
||||
}
|
||||
// Находит все предложения
|
||||
// указанных рецептов
|
||||
POST /v1/offers/search
|
||||
{
|
||||
"location",
|
||||
"recipes": [
|
||||
"espresso",
|
||||
"lungo"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Результатом подобных политик «тихого» исправления ошибок становятся абсурдные ситуации типа «null island» — [самой посещаемой точки в мире](https://www.sciencealert.com/welcome-to-null-island-the-most-visited-place-that-doesn-t-exist). Чем популярнее API, тем больше шансов, что партнеры просто не обратят внимания на такие пограничные ситуации.
|
||||
Представим теперь, что вызов первого метода вернул пустой массив результатов, т.е. ни одного рецепта кофе, удовлетворяющего условиям, не было найдено. Хорошо, если разработчик партнёра предусмотрит эту ситуацию и не будет делать запрос поиска предложений — но мы не можем быть стопроцентно в этом уверены. Если обработка пустого массива рецептов не предусмотрена, то приложение партнёра выполнит вот такой запрос:
|
||||
|
||||
**Хорошо**
|
||||
```
|
||||
POST /v1/offers/search
|
||||
{
|
||||
"location",
|
||||
"recipes": []
|
||||
}
|
||||
```
|
||||
|
||||
Часто можно столкнуться с ситуацией, когда эндпойнт просто проигнорирует наличие пустого массива `recipes` и вернёт предложения так, как будто никакого фильтра по рецепту передано не было. В нашем примере это будет означать, что приложение просто проигнорирует требование пользователя показать только напитки без молока, что мы никак не можем счесть приемлемым поведением. Поэтому ответом на такой запрос с пустым массивом в качестве параметра должна быть либо ошибка, либо пустой же массив предложений.
|
||||
|
||||
##### Валидируйте ввод
|
||||
|
||||
Какой из вариантов действий выбрать в предыдущем примере — исключение или пустой ответ — напрямую зависит от того, что записано в вашем контракте. Если спецификация прямо предписывает, что массив `recipes` должен быть непустым, то необходимо сгенерировать исключение (иначе вы фактически нарушаете собственную спецификацию!).
|
||||
|
||||
Это верно не только в случае непустых массивов, но и любых других зафиксированных в контракте ограничений. «Тихое» исправление недопустимых значений почти никогда не имеет никакого практического смысла:
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
POST /v1/offers/search
|
||||
{
|
||||
"location": {
|
||||
"longitude": 20,
|
||||
"latitude": 100
|
||||
}
|
||||
}
|
||||
→ 200 OK
|
||||
{
|
||||
// Предложения для точки
|
||||
// [0, 90]
|
||||
"offers"
|
||||
}
|
||||
```
|
||||
|
||||
Мы видим, что разработчик по какой-то причине передал некорректное значение долготы (100 градусов). Да, мы можем его «исправить», т.е. редуцировать до ближайшего допустимого значения (90 градусов), но кому от этого стало лучше? Разработчик никогда не узнает о допущенной ошибке, а конечному пользователю предложения кофе на Северном полюсе скорее всего нерелевантны.
|
||||
|
||||
**Хорошо**:
|
||||
```
|
||||
POST /v1/coffee-machines/search
|
||||
{
|
||||
"recipes": ["lngo"]
|
||||
// Положение пользователя не задано
|
||||
"location": {
|
||||
"longitude": 20,
|
||||
"latitude": 100
|
||||
}
|
||||
}
|
||||
→ 400 Bad Request
|
||||
{
|
||||
// описание ошибки
|
||||
// см. следующее правило
|
||||
}
|
||||
```
|
||||
|
||||
@@ -296,7 +353,7 @@ POST /v1/coffee-machines/search
|
||||
POST /v1/coffee-machines/search
|
||||
{
|
||||
"recipes": ["lngo"]
|
||||
"position": {
|
||||
"location": {
|
||||
"latitude": 0,
|
||||
"longitude": 0
|
||||
},
|
||||
@@ -307,7 +364,7 @@ POST /v1/coffee-machines/search
|
||||
"results": [],
|
||||
"warnings": [{
|
||||
"type": "suspicious_coordinates",
|
||||
"message": "Position [0, 0]⮠
|
||||
"message": "Location [0, 0]⮠
|
||||
is probably a mistake"
|
||||
}, {
|
||||
"type": "unknown_field",
|
||||
@@ -325,7 +382,7 @@ POST /v1/coffee-machines/search⮠
|
||||
strict_mode=true
|
||||
{
|
||||
"recipes": ["lngo"]
|
||||
"position": {
|
||||
"location": {
|
||||
"latitude": 0,
|
||||
"longitude": 0
|
||||
}
|
||||
@@ -334,7 +391,7 @@ POST /v1/coffee-machines/search⮠
|
||||
{
|
||||
"errors": [{
|
||||
"type": "suspicious_coordinates",
|
||||
"message": "Position [0, 0]⮠
|
||||
"message": "Location [0, 0]⮠
|
||||
is probably a mistake"
|
||||
}],
|
||||
…
|
||||
@@ -349,6 +406,41 @@ POST /v1/coffee-machines/search⮠
|
||||
disable_errors=suspicious_coordinates
|
||||
```
|
||||
|
||||
##### Значения по умолчанию должны быть осмысленны
|
||||
|
||||
Значения по умолчанию — один из самых ваших сильных инструментов, позволяющих избежать многословности при работе с API. Однако эти умолчания должны помогать разработчикам, а не маскировать их ошибки.
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
POST /v1/coffee-machines/search
|
||||
{
|
||||
"recipes": ["lungo"]
|
||||
// Положение пользователя не задано
|
||||
}
|
||||
→
|
||||
{
|
||||
"results": [
|
||||
// Результаты для какой-то
|
||||
// локации по умолчанию
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Формально, подобное умолчание допустимо — почему бы не иметь концепции «географических координат по умолчанию». Однако в реальности результатом подобных политик «тихого» исправления ошибок становятся абсурдные ситуации типа «null island» — [самой посещаемой точки в мире](https://www.sciencealert.com/welcome-to-null-island-the-most-visited-place-that-doesn-t-exist). Чем популярнее API, тем больше шансов, что партнеры просто не обратят внимания на такие пограничные ситуации.
|
||||
|
||||
**Хорошо**:
|
||||
```
|
||||
POST /v1/coffee-machines/search
|
||||
{
|
||||
"recipes": ["lungo"]
|
||||
// Положение пользователя не задано
|
||||
}
|
||||
→ 400 Bad Request
|
||||
{
|
||||
// описание ошибки
|
||||
}
|
||||
```
|
||||
|
||||
##### Ошибки должны быть информативными
|
||||
|
||||
Недостаточно просто валидировать ввод — необходимо ещё и уметь правильно описать, в чём состоит проблема. В ходе работы над интеграцией партнёры неизбежно будут допускать детские ошибки. Чем понятнее тексты сообщений, возвращаемых вашим API, тем меньше времени разработчик потратит на отладку, и тем приятнее работать с таким API.
|
||||
@@ -402,14 +494,6 @@ POST /v1/coffee-machines/search
|
||||
```
|
||||
Также хорошей практикой является указание всех допущенных ошибок, а не только первой найденной.
|
||||
|
||||
##### Декларируйте технические ограничения явно
|
||||
|
||||
У любого поля в вашем API есть ограничения на допустимые значения: максимальная длина текста, объём прикладываемых документов в мегабайтах, разрешённые диапазоны цифровых значений. Часто разработчики API пренебрегают указанием этих лимитов — либо потому, что считают их очевидными, либо потому, что попросту не знают их сами. Это, разумеется, один большой антипаттерн: незнание пределов использования системы автоматически означает, что код партнёров может в любой момент перестать работать по не зависящим от них причинам.
|
||||
|
||||
Поэтому, во-первых, указывайте границы допустимых значений для всех без исключения полей в API, и, во-вторых, если эти границы нарушены, генерируйте машиночитаемую ошибку с описанием, какое ограничение на какое поле было нарушено.
|
||||
|
||||
То же соображение применимо и к квотам: партнёры должны иметь доступ к информации о том, какую долю доступных ресурсов они выбрали, и ошибки в случае превышения квоты должны быть информативными.
|
||||
|
||||
##### Указывайте время жизни ресурсов и политики кэширования
|
||||
|
||||
В современных системах клиент, как правило, обладает собственным состоянием и почти всегда кэширует результаты запросов — неважно, долговременно ли или в течение сессии: у каждого объекта всегда есть какое-то время автономной жизни. Поэтому желательно вносить ясность; каким образом рекомендуется кэшировать результат должно быть понятно, если не из сигнатур операций, то хотя бы из документации.
|
||||
@@ -509,6 +593,7 @@ PUT /v1/orders/drafts⮠
|
||||
* клиент ошибся, пытаясь применить конфликтующие изменения.
|
||||
|
||||
Рассмотрим следующий пример: представим, что у нас есть ресурс с общим доступом, контролируемым посредством номера ревизии, и клиент пытается его обновить.
|
||||
|
||||
```
|
||||
POST /resource/updates
|
||||
{
|
||||
@@ -521,7 +606,8 @@ POST /resource/updates
|
||||
|
||||
Сервер мог бы попытаться сравнить значения поля `updates`, предполагая, что одинаковые значения означают перезапрос, но это предположение будет опасно неверным (например, если ресурс представляет собой счётчик, то последовательные запросы с идентичным телом нормальны).
|
||||
|
||||
Добавление токена идемпотентности (явного в виде случайной строки или неявного в виде черновиков) решает эту проблему
|
||||
Добавление токена идемпотентности (явного в виде случайной строки или неявного в виде черновиков) решает эту проблему:
|
||||
|
||||
```
|
||||
POST /resource/updates
|
||||
X-Idempotency-Token: <токен>
|
||||
@@ -553,7 +639,7 @@ X-Idempotency-Token: <токен>
|
||||
|
||||
##### Не изобретайте безопасность
|
||||
|
||||
Если бы автору этой книги давали доллар каждый раз, когда ему приходилось бы имплементировать кем-то придуманный дополнительный протокол безопасности — он бы давно уже был на заслуженной пенсии. Любовь разработчиков API к подписыванию параметры запросов или сложным схемам обмена паролей на токены столь же несомненна, сколько и бессмысленна.
|
||||
Если бы автору этой книги давали доллар каждый раз, когда ему приходилось бы имплементировать кем-то придуманный дополнительный протокол безопасности — он бы давно уже был на заслуженной пенсии. Любовь разработчиков API к подписыванию параметров запросов или сложным схемам обмена паролей на токены столь же несомненна, сколько и бессмысленна.
|
||||
|
||||
**Во-первых**, почти всегда процедуры, обеспечивающие безопасность той или иной операции, *уже разработаны*. Нет никакой нужды придумывать их заново, просто имплементируйте какой-то из существующих протоколов. Никакие самописные алгоритмы проверки сигнатур запросов не обеспечат вам того же уровня защиты от атаки [Man-in-the-Middle](https://en.wikipedia.org/wiki/Man-in-the-middle_attack), как соединение по протоколу TLS с взаимной проверкой сигнатур сертификатов.
|
||||
|
||||
@@ -581,11 +667,11 @@ GET /v1/partner-api/{partner-id}⮠
|
||||
"<script>alert(document.cookie)</script>"
|
||||
```
|
||||
|
||||
Подобный интерфейс является прямым способом соорудить хранимую XSS, которым потенциально может воспользоваться злоумышленник. Да, это ответственность самого партнёра — не допускать сохранения подобного ввода. Но большие цифры по-прежнему работают против вас: всегда найдутся начинающие разработчики, которые не знают об этом виде уязвимости или не подумали о нём. В худшем случае существование таких хранимых уязвимостей может затронуть не только конкретного партнёра, но и вообще всех пользователей API.
|
||||
Подобный интерфейс является прямым способом соорудить хранимую XSS, которым потенциально может воспользоваться злоумышленник. Да, это ответственность самого партнёра — не допускать сохранения подобного ввода и/или экранировать его при выводе. Но большие цифры по-прежнему работают против вас: всегда найдутся начинающие разработчики, которые не знают об этом виде уязвимости или не подумали о нём. В худшем случае существование таких хранимых уязвимостей может затронуть не только конкретного партнёра, но и вообще всех пользователей API.
|
||||
|
||||
В таких ситуациях мы рекомендуем, во-первых, всегда валидировать вводимые через API данные, и, во-вторых, ограничивать радиус взрыва так, чтобы через уязвимости в коде одного партнёра нельзя было затронуть других партнёров. В случае, если функциональность небезопасного ввода всё же нужна, необходимо предупреждать о рисках максимально явно.
|
||||
В таких ситуациях мы рекомендуем, во-первых, всегда экранировать вводимые через API данные, и, во-вторых, ограничивать радиус взрыва так, чтобы через уязвимости в коде одного партнёра нельзя было затронуть других партнёров. В случае, если функциональность небезопасного ввода всё же нужна, необходимо предупреждать о рисках максимально явно.
|
||||
|
||||
**Луче** (но не идеально):
|
||||
**Лучше** (но не идеально):
|
||||
```
|
||||
// Позволяет партнёру задать
|
||||
// потенциально небезопасное
|
||||
|
125
src/ru/drafts/02-Раздел II. Обратная совместимость/01.md
Normal file
125
src/ru/drafts/02-Раздел II. Обратная совместимость/01.md
Normal file
@@ -0,0 +1,125 @@
|
||||
### Постановка проблемы обратной совместимости
|
||||
|
||||
Как обычно, дадим смысловое определение «обратной совместимости», прежде чем начинать изложение.
|
||||
|
||||
Обратная совместимость — это свойство всей системы API быть стабильной во времени. Это значит следующее: **код, написанный разработчиками с использованием вашего API, продолжает работать функционально корректно в течение длительного времени**. К этому определению есть два больших вопроса, и два уточнения к ним.
|
||||
|
||||
1. Что значит «функционально корректно»?
|
||||
|
||||
Это значит, что код продолжает выполнять свою функцию — решать какую-то задачу пользователя. Это не означает, что он продолжает работать одинаково: например, если вы предоставляете UI-библиотеку, то изменение функционально несущественных деталей дизайна, типа глубины теней или формы штриха границы, обратную совместимость не нарушит. А вот, например, изменение размеров визуальных компонентов, скорее всего, приведёт к тому, что какие-то пользовательские макеты развалятся.
|
||||
|
||||
2. Что значит «длительное время»?
|
||||
|
||||
С нашей точки зрения длительность поддержания обратной совместимости следует увязывать с длительностью жизненных циклов приложений в соответствующей предметной области. Хороший ориентир в большинстве случаев — это LTS-периоды платформ. Так как приложение всё равно будет переписано в связи с окончанием поддержки платформы, нормально предложить также и переход на новую версию API. В основных предметных областях (десктопные и мобильные операционные системы) этот срок исчисляется несколькими годами.
|
||||
|
||||
Почему обратную совместимость необходимо поддерживать (в том числе предпринимать необходимые меры ещё на этапе проектирования API) — понятно из определения. Прекращение работы приложения (полное или частичное) по вине поставщика API — крайне неприятное событие, а то и катастрофа, для любого разработчика, особенно если он платит за этот API деньги.
|
||||
|
||||
Но развернём теперь проблему в другую сторону: а почему вообще возникает проблема с поддержанием обратной совместимости? Почему мы можем *хотеть* её нарушить? Ответ на этот вопрос, при кажущейся простоте, намного сложнее, чем на предыдущий.
|
||||
|
||||
Мы могли бы сказать, что *обратную совместимость приходится нарушать для расширения функциональности API*. Но это лукавство: новая функциональность на то и *новая*, что она не может затронуть код приложений, который её не использует. Да, конечно, есть ряд сопутствующих проблем, приводящих к стремлению переписать *наш* код, код самого API, с выпуском новой мажорной версии:
|
||||
|
||||
* код банально морально устарел, внесение в него изменений, пусть даже в виде расширения функциональности, нецелесообразно;
|
||||
|
||||
* новая функциональность не была предусмотрена в старом интерфейсе: мы хотели бы наделить уже существующие сущности новыми свойствами, но не можем;
|
||||
|
||||
* наконец, за прошедшее после изначального релиза время мы узнали о предметной области и практике применения нашего API гораздо больше, и сделали бы многие вещи иначе.
|
||||
|
||||
Эти аргументы можно обобщить как «разработчики API не хотят работать со старым кодом», не сильно покривив душой. Но и это объяснение неполно: даже если вы не собираетесь переписывать код API при добавлении новой функциональности, или вы вовсе её и не собирались добавлять, выпускать новые версии API — мажорные и минорные — всё равно придётся.
|
||||
|
||||
**NB**: в рамках этой главы мы не разделяем минорные версии и патчи: под словами «минорная версия» имеется в виду любой обратно совместимый релиз API.
|
||||
|
||||
Напомним, что [API — это мост](https://twirl.github.io/The-API-Book/docs/API.ru.html#chapter-2), средство соединения разных программируемых контекстов. И как бы нам ни хотелось зафиксировать конструкцию моста, наши возможности ограничены: мост-то мы можем зафиксировать — да вот края ущелья, как и само ущелье, не можем. В этом корень проблемы: мы не можем оставить *свой* код без изменений, поэтому нам придётся рано или поздно потребовать, чтобы клиенты изменили *свой*.
|
||||
|
||||
Помимо наших собственных поползновений в сторону изменения архитектуры API, три других тектонических процесса происходят одновременно: размывание клиентов, предметной области и нижележащей платформы.
|
||||
|
||||
#### Фрагментация клиентских приложений
|
||||
|
||||
В тот момент, когда вы выпустили первую версию API, и первые клиенты начали использовать её — ситуация идеальна. Есть только одна версия, и все клиенты работают с ней. А вот дальше возможны два варианта развития событий:
|
||||
|
||||
1. Если платформа поддерживает on-demand получение кода, как старый-добрый Веб, и вы не поленились это получение кода реализовать (в виде платформенного SDK, например, JS API), то развитие API более или менее находится под вашим контролем. Поддержание обратной совместимости сводится к поддержанию обратной совместимости *клиентской библиотеки*, а вот в части сервера и клиент-серверного взаимодействия вы свободны.
|
||||
|
||||
Это не означает, что вы не можете нарушить обратную совместимость — всё ещё можно напортачить с заголовками кэширования SDK или банально допустить баг в коде. Кроме того, даже on-demand системы всё равно не обновляются мгновенно — автор сталкивался с ситуацией, когда пользователи намеренно держали вкладку браузера открытой *неделями*, чтобы не обновляться на новые версии. Тем не менее, вам почти не придётся поддерживать более двух (последней и предпоследней) минорных версий клиентского SDK. Более того, вы можете попытаться в какой-то момент переписать предыдущую мажорную версию библиотеки, имплементировав её на основе API новой версии.
|
||||
|
||||
2. Если поддержка on-demand кода платформой не поддерживается или запрещена условиями, как это произошло с современными мобильными платформами, то ситуация становится гораздо сложнее. По сути, каждый клиент — это «слепок» кода, который работает с вашим API, зафиксированный в том состоянии, в котором он был на момент компиляции. Обновление клиентских приложений по времени растянуто гораздо дольше, нежели Web-приложений; самое неприятное здесь состоит в том, что некоторые клиенты *не обновятся вообще никогда* — по одной из трёх причин:
|
||||
|
||||
* разработчики просто не выпускают новую версию приложения, его развитие заморожено;
|
||||
* пользователь не хочет обновляться (в том числе потому, что, по мнению пользователя, разработчики приложения его «испортили» в новых версиях);
|
||||
* пользователь не может обновиться вообще, потому что его устройство больше не поддерживается.
|
||||
|
||||
В современных реалиях все три категории в сумме легко могут составлять десятки процентов аудитории. Это означает, что прекращение поддержки любой версии API является весьма заметным событием — особенно если приложения разработчика поддерживают более широкий спектр версий платформы, нежели ваш API.
|
||||
|
||||
Вы можете не выпускать вообще никаких SDK, предоставляя только серверный API в виде, например, HTTP эндпойнтов. Вам может показаться, что таким образом, пусть ваш API и стал менее конкурентоспособным на рынке из-за отсутствия SDK, вы облегчили себе задачу поддержания обратной совместимости. На самом деле это совершенно не так: раз вы не предоставляете свой SDK — или разработчики возьмут неофициальный SDK (если кто-то его сделает), или просто каждый из них напишет по фреймворку. Стратегия «ваш фреймворк — ваша ответственность», к счастью или к сожалению, работает плохо: если на вашем API пишут некачественные приложения — значит, ваш API сам некачественный. Уж точно по мнению разработчиков, а может и по мнению пользователей, если работа API внутри приложения пользователю видна.
|
||||
|
||||
Конечно, если ваш API достаточно stateless и не требует клиентских SDK (или же можно обойтись просто автогенерацией SDK из спецификации), эти проблемы будут гораздо менее заметны, но избежать их полностью можно только одним способом — никогда не выпуская новых версий API. Во всех остальных случаях вы будете иметь дело с какой-то гребёнкой распределения количества пользователей по версиям API и версиям SDK.
|
||||
|
||||
#### Эволюция предметной области
|
||||
|
||||
Другая сторона ущелья — та самая нижележащая функциональность, к которой вы предоставляете API. Она, разумеется, тоже не статична и развивается в какую-то сторону:
|
||||
|
||||
* появляется новая функциональность;
|
||||
* старая функциональность перестаёт поддерживаться;
|
||||
* меняются интерфейсы.
|
||||
|
||||
Как правило, API изначально покрывает только какую-то часть существующей предметной области. В случае нашего [примера с API кофемашин](https://twirl.github.io/The-API-Book/docs/API.ru.html#chapter-7) разумно ожидать, что будут появляться новые модели с новым API, которые нам придётся включать в свою платформу, и гарантировать возможность сохранения того же интерфейса абстракции — весьма непросто. Даже если просто добавлять поддержку новых видов нижележащих устройств, не добавляя ничего во внешний интерфейс — это всё равно изменения в коде, которые могут в итоге привести к несовместимости, пусть и ненамеренно.
|
||||
|
||||
Стоит также отметить, что далеко не все поставщики API относятся к поддержанию обратной совместимости, да и вообще к качеству своего ПО, так же серьёзно, как и (надеемся) вы. Стоит быть готовым к тому, что заниматься поддержанием вашего API в рабочем состоянии, то есть написанием и поддержкой фасадов к меняющемуся ландшафту предметной области, придётся именно вам, и зачастую довольно внезапно.
|
||||
|
||||
#### Дрифт платформ
|
||||
|
||||
Наконец, есть и третья сторона вопроса — «ущелье», через которое вы перекинули свой мост в виде API. Код, который напишут разработчики, исполняется в некоторой среде, которую вы не можете контролировать, и она тоже эволюционирует. Появляются новые версии операционной системы, браузеров, протоколов, языка SDK. Разрабатываются новые стандарты и принимаются новые соглашения, некоторые из которых сами по себе обратно несовместимы, и поделать с этим ничего нельзя.
|
||||
|
||||
Как и в случае со старыми версиями приложений, старые версии платформ также приводят к фрагментации, поскольку разработчикам (в том числе и разработчикам API) объективно тяжело поддерживать старые платформы, а пользователям столь же объективно тяжело обновляться, так как обновление операционной системы зачастую невозможно без замены самого устройства на более новое.
|
||||
|
||||
Самое неприятное во всём этом то, что к изменениям в API подталкивает не только поступательный прогресс в виде новых платформ и протоколов, но и банальная мода и вкусовщина. Буквально несколько лет назад были в моде объёмные реалистичные иконки, от которых все отказались в пользу плоских и абстрактных — и большинству разработчиков визуальных компонентов пришлось, вслед за модой, переделывать свои библиотеки, выпуская новые наборы иконок или заменяя старые. Аналогично прямо сейчас повсеместно внедряется поддержка «ночных» тем интерфейсов, что требует изменений в большом количестве API.
|
||||
|
||||
#### Обратная совместимость на уровне спецификаций
|
||||
|
||||
В случае применения API-first подхода, понятие «обратная совместимость» обретает дополнительное измерение, поскольку теперь в системе помимо двух связываемых контекстов появляется ещё спецификация и кодогенерация по ней. Становится возможным нарушить обратную совместимость, не нарушая спецификации (например, изменив строгую консистентность на слабую) — и, напротив, несовместимо изменить спецификацию, не затронув никак существующие интеграции (например, изменив `additionalProperties` с `false` на `true` в OpenAPI).
|
||||
|
||||
Вообще вопрос того, являются ли две версии спецификации обратно совместимыми — относится скорее к серой зоне, поскольку в самих стандартах спецификаций такое понятие не определено. Из общих соображений, утверждение «изменение спецификации является обратно-совместимым» тождественно утверждению «любой клиентский код, написанный или сгенерированный по этой спецификации, продолжит работать функционально корректно после релиза сервера, соответствующего обновлённой версии спецификации», однако в практическом смысле следовать этому определению достаточно тяжело. Изучить поведение всех мыслимых генераторов кода по спецификациям крайне трудоёмко (в частности, очень сложно предсказать, переживёт ли сгенерированный код упомянутое выше изменение `additionaProperties` на `true` с последующей передачей дополнительных полей).
|
||||
|
||||
Таким образом, использование IDL для описания API при всех плюсах этого подхода приводит к ещё одной существенной проблеме дрифта технологий: версии IDL и, что важнее, основанного на нём программного обеспечения, тоже постоянно обновляются, и далеко не всегда предсказуемым образом.
|
||||
|
||||
**NB**: мы здесь склонны советовать придерживаться разумного подхода, а именно — не использовать потенциально проблемные с точки зрения обратной совместимости возможности (включая упомянутый `additionalProperties: false`) и при оценке совместимости изменений исходить из соображения, что сгенерированный по спецификации код ведёт себя так же, как и написанный вручную. В случае же неразрешимых сомнений вам не остаётся ничего другого, кроме как перебрать все имеющиеся кодогенераторы и проверить работоспособность их выдачи.
|
||||
|
||||
#### Политика обратной совместимости
|
||||
|
||||
Итого, если суммировать:
|
||||
* вследствие итерационного развития приложений, платформ и предметной области вы будете вынуждены выпускать новые версии вашего API; в разных предметных областях скорость развития разная, но почти никогда не нулевая;
|
||||
* вкупе это приведёт к фрагментации используемой версии API по приложениям и платформам;
|
||||
* вам придётся принимать решения, критически влияющие на надёжность вашего API в глазах потребителей.
|
||||
|
||||
Опишем кратко эти решения и ключевые принципы их принятия.
|
||||
|
||||
1. Как часто выпускать мажорные версии API.
|
||||
|
||||
Это в основном *продуктовый* вопрос. Новая мажорная версия API выпускается, когда накоплена критическая масса функциональности, которую невозможно или слишком дорого поддерживать в рамках предыдущей мажорной версии. В стабильной ситуации такая необходимость возникает, как правило, раз в несколько лет. На динамично развивающихся рынках новые мажорные версии можно выпускать чаще, здесь ограничителем являются только ваши возможности выделить достаточно ресурсов для поддержания зоопарка версий. Однако следует заметить, что выпуск новой мажорной версии раньше, чем была стабилизирована предыдущая (а на это, как правило, требуется от нескольких месяцев до года), выглядит для разработчиков очень плохим сигналом, означающим риск *постоянно* сидеть на сырой платформе.
|
||||
|
||||
2. Какое количество *мажорных* версий поддерживать одновременно.
|
||||
|
||||
Что касается мажорных версий, то *теоретический* ответ мы дали выше: в идеальной ситуации жизненный цикл мажорной версии должен быть чуть длиннее жизненного цикла платформы. Для стабильных ниш типа десктопных операционных систем это порядка 5-10 лет, для новых и динамически развивающихся — меньше, но всё равно измеряется в годах. *Практически* следует смотреть на долю потребителей, реально продолжающих пользоваться версией.
|
||||
|
||||
3. Какое количество *минорных* версий (в рамках одной мажорной) поддерживать одновременно.
|
||||
|
||||
Для минорных версий возможны два варианта:
|
||||
|
||||
* если вы предоставляете только серверный API и компилируемые SDK, вы можете в принципе не поддерживать никакие минорные версии API, помимо актуальной: серверный API находится полностью под вашим контролем, и вы можете оперативно исправить любые проблемы с логикой;
|
||||
* если вы предоставляете code-on-demand SDK, то вот здесь хорошим тоном является поддержка предыдущих минорных версий SDK в работающем состоянии на срок, достаточный для того, чтобы разработчики могли протестировать своё приложение с новой версией и внести какие-то правки по необходимости. Так как полностью переписывать приложения при этом не надо, разумно ориентироваться на длину релизных циклов в вашей индустрии, обычно это несколько месяцев в худшем случае.
|
||||
|
||||
#### Одновременный доступ к нескольким минорным версиям API
|
||||
|
||||
В современной промышленной разработке, особенно если мы говорим о внутренних API, новая версия, как правило, полностью заменяет предыдущую. Если в новой версии обнаруживаются критические ошибки, она может быть откачена (путём релиза предыдущей версии), но одновременно две сборки не сосуществуют. В случае публичных API такой подход становится тем более опасным, чем больше партнёров используют API.
|
||||
|
||||
В самом деле, с ростом количества потребителей подход «откатить проблемную версию API в случае массовых жалоб» становится всё более деструктивным. Для партнёров, вообще говоря, оптимальным вариантом является жёсткая фиксация той версии API, для которой функциональность приложения была протестирована (и чтобы поставщик API при этом как-то незаметно исправлял возможные проблемы с информационной безопасностью и приводил своё ПО в соответствие с вновь возникающими законами).
|
||||
|
||||
**NB**. Из тех же соображений следует, что для популярных API также становится всё более желательным предоставление возможности подключать бета-, а может быть и альфа-версии для того, чтобы у партнёров была возможность заранее понять, какие проблемы ожидают их с релизом новой версии API.
|
||||
|
||||
Несомненный и очень важный плюс semver состоит в том, что она предоставляет возможность подключать версии с нужной гранулярностью:
|
||||
|
||||
* указание первой цифры (мажорной версии) позволяет гарантировать получение обратно совместимой версии API;
|
||||
* указание двух цифр (минорной и мажорной версий) позволяет получить не просто обратно совместимую версию, но и необходимую функциональность, которая была добавлена уже после начального релиза;
|
||||
* наконец, указание всех трёх цифр (мажорной, минорной и патча) позволяет жёстко зафиксировать конкретный релиз API, со всеми возможными особенностями (и ошибками) в его работе, что — теоретически — означает, что работоспособность интеграции не будет нарушена, пока эта версия физически доступна.
|
||||
|
||||
Понятно, что бесконечное сохранение минорных версий в большинстве случаев невозможно (в т.ч. из-за накапливающихся проблем с безопасностью и соответствием законодательству), однако предоставление такого доступа в течение разумного времени для больших API является гигиенической нормой.
|
||||
|
||||
**NB**. Часто в защиту политики только одной доступной версии API можно услышать аргумент о том, что кодом SDK или сервера API проблема обратной совместимости не исчерпывается, т.к. он может опираться на какие-то неверсионируемые сервисы (например, на какие-то данные в БД, которые должны разделяться между всеми версиями API) или другие API, придерживающиеся менее строгих политик. Это соображение, на самом деле, является лишь дополнительным аргументом в пользу изоляции таких зависимостей (см. главу «Блокнот душевного покоя»), поскольку это означает только лишь то, что изменения в этих подсистемах могут привести к неработоспособности API сами по себе.
|
11
src/ru/drafts/03-Раздел III. Паттерны API/01.md
Normal file
11
src/ru/drafts/03-Раздел III. Паттерны API/01.md
Normal file
@@ -0,0 +1,11 @@
|
||||
### О паттернах проектирования в контексте API
|
||||
|
||||
Термин [«паттерны»](https://en.wikipedia.org/wiki/Software_design_pattern#History) применительно к разработке программного обеспечения был введён Кентом Бэком и Уордом Каннингемом в 1987 году, и популяризирован «бандой четырёх» (Эрих Гамма, Ричард Хелм, Ральф Джонсон и Джон Влиссидес) в их книге «Приёмы объектно-ориентированного проектирования. Паттерны проектирования», изданной в 1994 году. Согласно общепринятому определению, паттерны программирования — «повторяемая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто возникающего контекста».
|
||||
|
||||
Если мы говорим об API, особенно если их конечным потребителем является разработчик (API фреймворков, операционных систем), классические паттерны проектирования вполне к ним применимы. И действительно, многие описанные в предыдущем разделе примеры представляют собой применение того или иного паттерна.
|
||||
|
||||
Однако, если мы подойдём с другой стороны и попытаемся поговорить о частых проблемах проектирования API, то увидим, что многие из них не сводятся к базовым паттернам разработки ПО. Скажем, проблемы кэширования ресурсов (и инвалидации кэша) или организация асинхронного взаимодействия классиками не покрыты.
|
||||
|
||||
В рамках этого раздела мы попытаемся описать те задачи проектирования API и подходы к их решению, которые представляются нам наиболее важными. Мы понимаем, что читатель, знакомый с классическими трудами «банды четырёх», Гради Буча и Мартина Фаулера ожидает от раздела с названием «Паттерны API» большей глубины и ширины охвата, и заранее просим у него прощения.
|
||||
|
||||
**NB**: первый из подобных паттернов — это API-first подход к разработке ПО, который мы описали во введении.
|
25
src/ru/drafts/03-Раздел III. Паттерны API/02.md
Normal file
25
src/ru/drafts/03-Раздел III. Паттерны API/02.md
Normal file
@@ -0,0 +1,25 @@
|
||||
### API-first подход с технической точки зрения
|
||||
|
||||
Первый и самый важный паттерн разработки API — это концепция «API-first», т.е. собственно парадигма разработки ПО, в которой главной составляющей является разработка API.
|
||||
|
||||
Следует, однако, различать API-first подход в продуктовом и техническом смысле. Первое означает, что при разработке некоторого сервиса сначала как первый (и иногда единственный) шаг разрабатывается API к нему, и мы обсудим этот подход в разделе «API как продукт».
|
||||
|
||||
Если же мы говорим об API-first подходе в техническом смысле, то мы имеем в виду следующее: **контракт, т.е. обязательство связывать программные контексты, предшествует реализации и определяет её**. Конкретнее, речь идёт о двух принципах:
|
||||
* контракт разрабатывается и фиксируется в виде спецификации до того, как функциональность непосредственно реализована;
|
||||
* если обнаруживается несоответствие контракта и его имплементации, изменения вносятся в имплементацию, а не в контракт.
|
||||
|
||||
Оба этих принципа фактически утверждают приоритет интересов партнёра-разработчика API:
|
||||
* первый принцип позволяет партнёру разрабатывать код по формальной спецификации, не требуя согласования с провайдером API;
|
||||
* появляется возможность использовать генерацию кода по спецификации, что может существенно упрощать и автоматизировать разработку;
|
||||
* партнёрский код может быть написан вообще в отсутствие доступа к API;
|
||||
* второй принцип позволяет не требовать изменений в коде партнёра, если обнаружен ошибка в реализации API.
|
||||
|
||||
Таким образом, API-first подход — это некоторая гарантия для ваших потребителей. Но, как легко заметить, работает эта гарантия только в условиях качественного дизайна API: если в фазе разработки спецификации были допущены неисправимые ошибки, то второй принцип придётся нарушить.
|
||||
|
||||
#### Обратная совместимость на уровне спецификаций
|
||||
|
||||
Другая возникающая проблема при адаптации API-first подхода — появление гранулярности понятия «обратная совместимость», поскольку теперь в системе помимо двух связываемых контекстов появляется ещё спецификация и кодогенерация по ней. Становится возможным нарушить обратную совместимость, не нарушая спецификации (например, изменив строгую консистентность на слабую) — и, напротив, несовместимо изменить спецификацию, не затронув никак существующие интеграции (например, изменив `additionalProperties` с `false` на `true` в OpenAPI).
|
||||
|
||||
Вообще вопрос того, являются ли две версии спецификации обратно совместимыми — относится скорее к серой зоне, поскольку в самих стандартах спецификаций такое понятие не определено. Из общих соображений, утверждение «изменение спецификации является обратно-совместимым» тождественно утверждению «любой клиентский код, написанный или сгенерированный по этой спецификации, продолжит работать функционально корректно после релиза сервера, соответствующего обновлённой версии спецификации», однако в практическом смысле следовать этому определению достаточно тяжело. Изучить поведение всех мыслимых генераторов кода по спецификациям крайне трудоёмко (в частности, очень сложно предсказать, переживёт ли сгенерированный код упомянутое выше изменение `additionaProperties` на `true` с последующей передачей дополнительных полей).
|
||||
|
||||
Мы здесь склонны советовать придерживаться разумного подхода, а именно — не использовать потенциально проблемные с точки зрения обратной совместимости возможности (включая упомянутый `additionalProperties: false`) и при оценке совместимости изменений исходить из соображения, что сгенерированный по спецификации код ведёт себя так же, как и написанный вручную. В случае же неразрешимых сомнений вам не остаётся ничего другого, кроме как перебрать все имеющиеся кодогенераторы и проверить работоспособность их выдачи.
|
Reference in New Issue
Block a user