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
style fix
This commit is contained in:
@@ -27,7 +27,7 @@ Cache-Control: no-cache
|
||||
```
|
||||
|
||||
Её следует читать так:
|
||||
* выполняется POST-запрос к ресурсу `/v1/bucket/{id}/some-resource`, где `{id}` заменяется на некоторый идентификатор `bucket`-а (при отсутствии уточнений подстановки вида `{something}` следует относить к ближайшему термину слева);
|
||||
* клиент выполняет POST-запрос к ресурсу `/v1/bucket/{id}/some-resource`, где `{id}` заменяется на некоторый идентификатор `bucket`-а (при отсутствии уточнений подстановки вида `{something}` следует относить к ближайшему термину слева);
|
||||
* запрос сопровождается (помимо стандартных заголовков, которые мы опускаем) дополнительным заголовком `X-Idempotency-Token`;
|
||||
* фразы в угловых скобках (`<токен идемпотентности>`) описывают семантику значения сущности (поля, заголовка, параметра);
|
||||
* в качестве тела запроса передаётся JSON, содержащий поле `some_parameter` со значением `value` и ещё какие-то поля, которые для краткости опущены (что показано многоточием);
|
||||
@@ -35,9 +35,11 @@ Cache-Control: no-cache
|
||||
* в ответе также могут находиться дополнительные заголовки, на которые мы обращаем внимание;
|
||||
* телом ответа является JSON, состоящий из единственного поля `error_message`; отсутствие значения поля означает, что его значением является именно то, что в этом поле и ожидается — в данном случае какое-то сообщение об ошибке.
|
||||
|
||||
Здесь термин «клиент» означает «приложение, установленное на устройстве пользователя, использующее рассматриваемое API». Приложение может быть как нативным, так и веб-приложением. Термины «агент» и «юзер-агент» являются синонимами термина «клиент».
|
||||
|
||||
Ответ (частично или целиком) и тело запроса могут быть опущены, если в контексте обсуждаемого вопроса их содержание не имеют значения.
|
||||
|
||||
Для упрощения возможна сокращенная запись вида: `POST /v1/bucket/{id}/some-resource` `{…,"some_parameter",…}` → `{ "operation_id" }`; тело запроса и/или ответа может опускаться аналогично полной записи.
|
||||
Возможна сокращённая запись вида: `POST /v1/bucket/{id}/some-resource` `{…,"some_parameter",…}` → `{ "operation_id" }`; тело запроса и/или ответа может опускаться аналогично полной записи.
|
||||
|
||||
Чтобы сослаться на это описание будут использоваться выражения типа «метод `POST /v1/bucket/{id}/some-resource`» или, для простоты, «метод `some-resource`» или «метод `bucket/some-resource`» (если никаких других `some-resource` в контексте главы не упоминается и перепутать не с чем).
|
||||
|
||||
|
@@ -7,10 +7,10 @@
|
||||
Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим?
|
||||
|
||||
1. Мы готовим с помощью нашего API *заказ* — один или несколько стаканов кофе — и взымаем за это плату.
|
||||
2. Каждый стакан кофе приготовлен по определённому рецепту, что подразумевает наличие разных ингредиентов и последовательности выполнения шагов приготовления.
|
||||
3. Напиток готовится на конкретной физической кофе-машине, располагающейся в какой-то точке пространства.
|
||||
2. Каждый стакан кофе приготовлен по определённому *рецепту*, что подразумевает наличие разных ингредиентов и последовательности выполнения шагов приготовления.
|
||||
3. Напиток готовится на конкретной физической *кофе-машине*, располагающейся в какой-то точке пространства.
|
||||
|
||||
Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы прежде всего стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей:
|
||||
Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы прежде всего стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей.
|
||||
|
||||
1. Упрощение работы разработчика и легкость обучения: в каждый момент времени разработчику достаточно будет оперировать только теми сущностями, которые нужны для решения его задачи; и наоборот, плохо выстроенная изоляция приводит к тому, что разработчику нужно держать в голове множество концепций, не имеющих прямого отношения к решаемой задаче.
|
||||
|
||||
@@ -28,14 +28,15 @@
|
||||
// размещает на указанной кофе-машине
|
||||
// заказ на приготовление лунго
|
||||
// и возвращает идентификатор заказа
|
||||
POST /v1/coffee-machines/orders?machine_id={id}
|
||||
POST /v1/coffee-machines/orders
|
||||
{
|
||||
"coffee_machine_id",
|
||||
"recipe": "lungo"
|
||||
}
|
||||
```
|
||||
```
|
||||
// возвращает состояние заказа
|
||||
GET /v1/orders?order_id={id}
|
||||
GET /v1/orders/{id}
|
||||
```
|
||||
|
||||
И зададимся вопросом, каким образом разработчик определит, что заказ клиента готов. Допустим, мы сделаем так: добавим в рецепт лунго эталонный объём, а в состояние заказа — количество уже налитого кофе. Тогда разработчику нужно будет проверить совпадение этих двух цифр, чтобы убедиться, что кофе готов.
|
||||
@@ -50,8 +51,9 @@
|
||||
|
||||
Вариант 2: мы модифицируем интерфейс, объявляя объём кофе, указанный в рецепте, значением по умолчанию; при размещении заказа мы разрешаем указать объём, отличный от эталонного:
|
||||
```
|
||||
POST /v1/coffee-machines/orders?machine_id={id}
|
||||
POST /v1/coffee-machines/orders
|
||||
{
|
||||
"coffee_machine_id",
|
||||
"recipe":"lungo",
|
||||
"volume":"800ml"
|
||||
}
|
||||
@@ -327,7 +329,7 @@ POST /v1/runtimes
|
||||
|
||||
Вернёмся к нашему примеру. Каким образом будет работать операция получения статуса заказа? Для получения статуса будет выполнена следующая цепочка вызовов:
|
||||
* пользователь вызовет метод `GET /v1/orders`;
|
||||
* обработчик `orders` выполнит операции своего уровня ответственности (проверку авторизации, в частности), найдёт идентификатор `program_run_id` и обратится к API программ `GET /v1/programs/{id}/runs/{program_run_id}`;
|
||||
* обработчик `orders` выполнит операции своего уровня ответственности (проверку авторизации, в частности), найдёт идентификатор `program_run_id` и обратится к API программ `runs/{program_run_id}`;
|
||||
* обработчик `runs` в свою очередь выполнит операции своего уровня (в частности, проверит тип API кофе-машины) и в зависимости от типа API пойдёт по одной из двух веток исполнения:
|
||||
* либо вызовет `GET /execution/status` физического API кофе-машины, получит объём кофе и сличит с эталонным;
|
||||
* либо обратится к `GET /v1/runtimes/{runtime_id}`, получит `state.status` и преобразует его к статусу заказа;
|
||||
@@ -346,7 +348,7 @@ POST /v1/runtimes
|
||||
* обработчик метода произведёт операции в своей зоне ответственности:
|
||||
* проверит авторизацию;
|
||||
* решит денежные вопросы — нужно ли делать рефанд;
|
||||
* найдёт идентификатор `program_run_id` и обратится к `POST /v1/programs/{id}/runs/{program_run_id}/cancel`;
|
||||
* найдёт идентификатор `program_run_id` и обратится к обработчику `runs/{program_run_id}/cancel`;
|
||||
* обработчик `runs/cancel` произведёт операции своего уровня (в частности, установит тип API кофе-машины) и в зависимости от типа API пойдёт по одной из двух веток исполнения:
|
||||
* либо вызовет `POST /execution/cancel` физического API кофе-машины;
|
||||
* либо вызовет `POST /v1/runtimes/{id}/terminate`;
|
@@ -74,7 +74,7 @@ POST /v1/orders/statistics/aggregate
|
||||
|
||||
Отдельное следствие из этого правила — денежные величины *всегда* должны сопровождаться указанием кода валюты.
|
||||
|
||||
Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что как ни сделай — кто-то останется недовольным. Классический пример такого рода — порядок географических координат ("широта-долгота" против "долгота-широта"). Здесь, увы, есть только один работающий метод борьбы с фрустрацией — «блокнот душевного спокойствия», который будет описан в разделе II.
|
||||
Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что, как ни сделай, — кто-то останется недовольным. Классический пример такого рода — порядок географических координат ("широта-долгота" против "долгота-широта"). Здесь, увы, есть только один работающий метод борьбы с фрустрацией — «блокнот душевного спокойствия», который будет описан в разделе II.
|
||||
|
||||
#### 3. Сохраняйте точность дробных чисел
|
||||
|
||||
@@ -187,7 +187,7 @@ GET /comments/{id}
|
||||
"content"
|
||||
}
|
||||
```
|
||||
— хотя операция будто бы выполнена успешна, клиенту необходимо сделать дополнительный запрос, чтобы понять необходимость решения капчи. Между вызовами `POST /comments` и `GET /comments/{id}` клиент находится в состоянии кота Шрёдингера: непонятно, опубликован комментарий или нет, и как отразить это пользователю.
|
||||
— хотя операция будто бы выполнена успешно, клиенту необходимо сделать дополнительный запрос, чтобы понять необходимость решения капчи. Между вызовами `POST /comments` и `GET /comments/{id}` клиент находится в состоянии кота Шрёдингера: непонятно, опубликован комментарий или нет, и как отразить это пользователю.
|
||||
|
||||
**Хорошо**:
|
||||
```
|
||||
@@ -206,7 +206,7 @@ GET /v1/comments/{id}
|
||||
…
|
||||
}
|
||||
```
|
||||
Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа невелик) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.
|
||||
Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа не оказывает значительного влияния на производительность) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.
|
||||
|
||||
#### 9. Идемпотентность
|
||||
|
||||
@@ -227,7 +227,7 @@ POST /orders
|
||||
POST /v1/orders
|
||||
X-Idempotency-Token: <случайная строка>
|
||||
```
|
||||
Клиент на своей стороне запоминает X-Idempotency-Token, и, в случае автоматического повторного перезапроса, обязан его сохранить. Сервер на своей стороне проверяет токен и, если заказ с таким токеном уже существует для этого клиента, не даёт создать заказ повторно.
|
||||
Клиент на своей стороне запоминает `X-Idempotency-Token`, и, в случае автоматического повторного перезапроса, обязан его сохранить. Сервер на своей стороне проверяет токен и, если заказ с таким токеном уже существует для этого клиента, не даёт создать заказ повторно.
|
||||
|
||||
**Альтернатива**:
|
||||
```
|
||||
@@ -263,7 +263,7 @@ GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
|
||||
* на каком расстоянии от указанной точки цена всё ещё действительна?
|
||||
|
||||
**Хорошо**:
|
||||
Для указания времени жизни кэша можно пользоваться стандатрными средствами протокола, например, заголовком Cache-Control. В ситуации, когда кэш нужен и по временной, и по пространственной координате следует поступить примерно так:
|
||||
Для указания времени жизни кэша можно пользоваться стандартными средствами протокола, например, заголовком `Cache-Control`. В ситуации, когда кэш нужен и по временной, и по пространственной координате следует поступить примерно так:
|
||||
```
|
||||
// Возвращает предложение: за какую сумму
|
||||
// наш сервис готов приготовить лунго
|
||||
@@ -298,20 +298,20 @@ GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
|
||||
// Возвращает указанный limit записей,
|
||||
// отсортированных по дате создания
|
||||
// начиная с записи с номером offset
|
||||
GET /records?limit=10&offset=100
|
||||
GET /v1/records?limit=10&offset=100
|
||||
```
|
||||
На первый взгляд это самый что ни на есть стандартный способ организации пагинации в API. Однако зададим себе три вопроса:
|
||||
1. Каким образом клиент узнает о появлении новых записей в начале списка?
|
||||
Легко заметить, что клиент может только попытаться повторить первый запрос и сличить идентификаторы с запомненным началом списка. Но что делать, если добавленное количество записей превышает limit? Представим себе ситуацию:
|
||||
Легко заметить, что клиент может только попытаться повторить первый запрос и сличить идентификаторы с запомненным началом списка. Но что делать, если добавленное количество записей превышает `limit`? Представим себе ситуацию:
|
||||
* клиент обрабатывает записи в порядке поступления;
|
||||
* произошла какая-то проблема, и накопилось большое количество необработанных записей;
|
||||
* клиент запрашивает новые записи (offset=0), однако не находит на первой странице известных идентификаторов — новых записей накопилось больше, чем limit;
|
||||
* клиент вынужден продолжить перебирать записи (увеличивая offset), пока не доберётся до последней известной ему; всё это время клиент простаивает;
|
||||
* клиент запрашивает новые записи (`offset=0`), однако не находит на первой странице известных идентификаторов — новых записей накопилось больше, чем `limit`;
|
||||
* клиент вынужден продолжить перебирать записи (увеличивая `offset`), пока не доберётся до последней известной ему; всё это время клиент простаивает;
|
||||
* таким образом может сложиться ситуация, когда клиент вообще никогда не обработает всю очередь, т.к. будет занят беспорядочным линейным перебором.
|
||||
2. Что произойдёт, если при переборе списка одна из записей в уже перебранной части будет удалена?
|
||||
Произойдёт следующее: клиент пропустит одну запись и никогда не сможет об этом узнать.
|
||||
3. Какие параметры кэширования мы можем выставить на этот эндпойнт?
|
||||
Никакие: повторяя запрос с теми же limit-offset, мы каждый раз получаем новый набор записей.
|
||||
Никакие: повторяя запрос с теми же `limit`-`offset`, мы каждый раз получаем новый набор записей.
|
||||
|
||||
**Хорошо**: в таких однонаправленных списках пагинация должна быть организована по тому ключу, порядок которых фиксирован. Например, вот так:
|
||||
```
|
||||
@@ -319,16 +319,41 @@ GET /records?limit=10&offset=100
|
||||
// отсортированных по дате создания,
|
||||
// начиная с первой записи, созданной позднее,
|
||||
// чем запись с указанным id
|
||||
GET /records?older_than={record_id}&limit=10
|
||||
GET /v1/records?older_than={record_id}&limit=10
|
||||
// Возвращает указанный limit записей,
|
||||
// отсортированных по дате создания,
|
||||
// начиная с первой записи, созданной раньше,
|
||||
// чем запись с указанным id
|
||||
GET /records?newer_than={record_id}&limit=10
|
||||
GET /v1/records?newer_than={record_id}&limit=10
|
||||
```
|
||||
При такой организации клиенту не надо заботиться об удалении или добавлении записей в уже перебранной части списка: он продолжает перебор по идентификатору известной записи — первой известной, если надо получить новые записи; последней известной, если надо продолжить перебор.
|
||||
Если операции удаления записей нет, то такие запросы можно свободно кэшировать — по одному и тому же URL будет всегда возвращаться один и тот же набор записей.
|
||||
Другой вариант организации таких списков — возврат курсора `cursor`, который используется вместо `record_id`, что делает интерфейсы универсальнее.
|
||||
Другой вариант организации таких списков — возврат курсора `cursor`, который используется вместо `record_id`, что делает интерфейсы более универсальными.
|
||||
```
|
||||
// Первый запрос данных
|
||||
POST /v1/records/list
|
||||
{
|
||||
// Какие-то дополнительные параметры фильтрации
|
||||
"filter": {
|
||||
"category": "some_category",
|
||||
"created_date": {
|
||||
"older_than": "2020-12-07"
|
||||
}
|
||||
}
|
||||
}
|
||||
→
|
||||
{
|
||||
"cursor"
|
||||
}
|
||||
```
|
||||
```
|
||||
// Последующие запросы
|
||||
GET /v1/records?cursor=<значение курсора>
|
||||
{ "records", "cursor" }
|
||||
```
|
||||
Достоинством схемы с курсором является возможно зашифровать в самом курсоре данные исходного запроса (т.е. `filter` в нашем примере), и таким образом не дублировать его в последующих запросах. Это может быть особенно актуально, если инициализирующий запрос готовит полный массив данных, например, перенося его из «холодного» хранилища в горячее.
|
||||
|
||||
Вообще схему с курсором можно реализовать множеством способов (например, не разделять первый и последующие запросы данных), главное — выбрать какой-то один.
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
@@ -437,7 +462,7 @@ POST /v1/coffee-machines/search
|
||||
|
||||
Следует иметь в виду, что явной передачи локации может оказаться недостаточно, поскольку в мире существуют территориальные конфликты и спорные территории. Каким образом API должно себя вести при попадании координат пользователя на такие территории — вопрос, к сожалению, в первую очередь юридический. Автору этой книги приходилось как-то разрабатывать API, в котором пришлось вводить концепцию «территория государства A по мнению официальных органов государства Б».
|
||||
|
||||
**Важно**: различайте локализацию для конечного пользователя и локализацию для разработчика. В примере из п. 12 сообщение `localized_message` адресовано пользователю — его должно показать приложение, если в коде обработка такой ошибки невозможно. Это сообщение должно быть написано на указанном в запросе языке и отформатировано согласно правилам локации пользователя. А вот сообщение `details.checks_failed[].message` написано не для пользователя, а для разработчика, который будет разбираться с проблемой. Соответственно, написано и отформатировано оно должно быть понятно для разработчика — что, скорее всего, означает «на английском языке», т.к. английский де факто является стандартом в мире разработки программного обеспечения.
|
||||
**Важно**: различайте локализацию для конечного пользователя и локализацию для разработчика. В примере из п. 12 сообщение `localized_message` адресовано пользователю — его должно показать приложение, если в коде обработка такой ошибки не предусмотрена. Это сообщение должно быть написано на указанном в запросе языке и отформатировано согласно правилам локации пользователя. А вот сообщение `details.checks_failed[].message` написано не для пользователя, а для разработчика, который будет разбираться с проблемой. Соответственно, написано и отформатировано оно должно быть понятным для разработчика образом — что, скорее всего, означает «на английском языке», т.к. английский де факто является стандартом в мире разработки программного обеспечения.
|
||||
|
||||
Следует отметить, что индикация, какие сообщения следует показать пользователю, а какие написаны для разработчика, должна, разумеется, быть явной конвенцией вашего API. В примере для этого используется префикс `localized_`.
|
||||
|
Reference in New Issue
Block a user