From 769dc1dada6caddf41c87d5fe5fde949bb56c911 Mon Sep 17 00:00:00 2001
From: Sergey Konstantinov Важно понимать, что вы вольны вводить свои собственные конвенции. Например, в некоторых фреймворках сознательно отказываются от парных методов Из названия любой сущности должно быть очевидно, что она делает и к каким сайд-эффектам может привести её использование. Плохо: Плохо: Неочевидно, что достаточно просто обращения к сущности Неочевидно, что достаточно просто обращения к сущности Хорошо: Плохо: Плохо: Стремитесь к тому, чтобы из сигнатуры функции было абсолютно ясно, что она делает, что принимает на вход и что возвращает. Вообще, при прочтении кода, работающего с вашим API, должно быть сразу понятно, что, собственно, он делает — без подглядывания в документацию. Два важных следствия: 1.1. Если операция модифицирующая, это должно быть очевидно из сигнатуры. В частности, не может быть модифицирующих операций за 1.2. Если в номенклатуре вашего API есть как синхронные операции, так и асинхронные, то (а)синхронность должна быть очевидна из сигнатур, либо должна существовать конвенция именования, позволяющая отличать синхронные операции от асинхронных. К сожалению, человечество не в состоянии договориться о таких простейших вещах, как «с какого дня начинается неделя», что уж говорить о каких-то более сложных стандартах. Поэтому всегда указывайте, по какому конкретно стандарту вы отдаёте те или иные величины. Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе: Поэтому всегда указывайте, по какому конкретно стандарту вы отдаёте те или иные величины. Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе. Плохо: Хорошо: Плохо: Хорошо: Отдельное следствие из этого правила — денежные величины всегда должны сопровождаться указанием кода валюты. Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что как ни сделай — кто-то останется недовольным. Классический пример такого рода — порядок географических координат ("широта-долгота" против "долгота-широта"). Здесь, увы, есть только один работающий метод борьбы с фрустрацией — «блокнот душевного спокойствия», который будет описан в разделе II. Там, где это позволено протоколом, дробные числа с фиксированной запятой — такие, как денежные суммы, например — должны передаваться в виде специально предназначенных для этого объектов, например, Decimal или аналогичных. Если в протоколе нет Decimal-типов (в частности, в JSON нет чисел с фиксированной запятой), следует либо привести к целому (путём домножения на указанный множитель), либо использовать строковый тип. Избегайте слов-«амёб» без определённой семантики, таких как get, apply, make. Сущности должны именоваться конкретно: Избегайте слов-«амёб» без определённой семантики, таких как get, apply, make. Сущности должны именоваться конкретно. Плохо: Хорошо: В XXI веке давно уже нет нужды называть переменные покороче. Плохо: Хорошо: Плохо: Возможно, автору этого API казалось, что аббревиатура Хорошо: Если поле называется Сущности-массивы должны именоваться во множественном числе или собирательными выражениями — Аналогично, если ожидается булево значение, то из названия это должно быть очевидно, т.е. именование должно описывать некоторое качественное состояние, например, Сущности-массивы должны именоваться во множественном числе или собирательными выражениями — Плохо: Хорошо: Аналогично, если ожидается булево значение, то из названия это должно быть очевидно, т.е. именование должно описывать некоторое качественное состояние, например, Плохо: Хорошо: Отдельно следует оговорить, что на разных платформах эти правила следует дополнить по-своему с учетом специфики first-class citizen-типов. Например, объекты типа Если наименование сущности само по себе является каким-либо термином, способным смутить разработчика, лучше добавить лишний префикс или постфикс, чтобы избежать непонимания. Плохо: Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует). Хорошо: Плохо: Плохо: Плохо: Хорошо: Плохо: Упражнение «как сделать эти интерфейсы хорошо» предоставим читателю. Упражнение «как сделать эти интерфейсы хорошо» предоставим читателю. Правило можно ещё сформулировать так: не заставляйте клиент гадать. Плохо: — хотя операция будто бы выполнена успешна, клиенту необходимо сделать дополнительный запрос, чтобы понять необходимость решения капчи. Между вызовами Хорошо: Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа невелик) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение. Все эндпойнты должны быть идемпотентны. Напомним, идемпотентность — это следующее свойство: повторный вызов той же операции с теми же параметрами не изменяет результат. Поскольку мы обсуждаем в первую очередь клиент-серверное взаимодействие, узким местом в котором является ненадежность сетевой составляющей, повтор запроса при обрыве соединения — не исключительная ситуация, а норма жизни. Все операции должны быть идемпотентны. Напомним, идемпотентность — это следующее свойство: повторный вызов той же операции с теми же параметрами не изменяет результат. Поскольку мы обсуждаем в первую очередь клиент-серверное взаимодействие, узким местом в котором является ненадежность сетевой составляющей, повтор запроса при обрыве соединения — не исключительная ситуация, а норма жизни. Там, где идемпотентность не может быть обеспечена естественным образом, необходимо добавить явный параметр — ключ идемпотентности или ревизию. Плохо: Повтор запроса создаст два заказа! Хорошо: Клиент на своей стороне запоминает X-Idempotency-Token, и, в случае автоматического повторного перезапроса, обязан его сохранить. Сервер на своей стороне проверяет токен и, если заказ с таким токеном уже существует для этого клиента, не даёт создать заказ повторно. Альтернатива: Создание черновика заказа — необязывающая операция, которая не приводит ни к каким последствиям, поэтому допустимо создавать черновики без токена идемпотентности.
+Операция подтверждения заказа — уже естественным образом идемпотентна, для неё В клиент-серверном API, как правило, сеть и ресурс сервера не бесконечны, поэтому кэширование на клиенте результатов операции является стандартным действием. Желательно в такой ситуации внести ясность; если не из сигнатур операций, то хотя бы из документации должно быть понятно, каким образом можно кэшировать результат. Плохо Плохо: Возникает два вопроса: Если на первый вопрос легко ответить введением стандартных заголовков Cache-Control, то для второго вопроса готовых решений нет. В ситуации, когда кэш нужен и по временной, и по пространственной координате следует поступить примерно так: Хорошо:
+Для указания времени жизни кэша можно пользоваться стандатрными средствами протокола, например, заголовком Cache-Control. В ситуации, когда кэш нужен и по временной, и по пространственной координате следует поступить примерно так: Любой эндпойнт, возвращающий массивы данных, должен содержать пагинацию. Никаких исключений в этом правиле быть не может. Любой эндпойнт, возвращающий изменяемые данные постранично, должен обеспечивать возможность эти данные перебрать. Плохо: Плохо: Хорошо: в таких однонаправленных списках пагинация должна быть организована по тому ключу, порядок которых фиксирован. Например, вот так: При такой организации клиенту не надо заботиться об удалении или добавлении записей в уже перебранной части списка: он продолжает перебор по идентификатору известной записи — первой известной, если надо получить новые записи; последней известной, если надо продолжить перебор.
Если операции удаления записей нет, то такие запросы можно свободно кэшировать — по одному и тому же URL будет всегда возвращаться один и тот же набор записей. Плохо:
- // возвращает рецепт лунго
GET /v1/recipes/lungo
// размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа
+
+ // размещает на указанной кофе-машине
+ // заказ на приготовление лунго
+ // и возвращает идентификатор заказа
POST /v1/coffee-machines/orders?machine_id={id}
{
"recipe": "lungo"
@@ -599,17 +611,16 @@ GET /sensors
set_entity
/ get_entity
в пользу одного метода entity
с опциональным параметром. Важно только проявить последовательность в её применении — если такая конвенция вводится, то абсолютно все методы API должны иметь подобную полиморфную сигнатуру, или по крайней мере должен существовать принцип именования, отличающий такие комбинированные методы от обычных вызовов.1. Явное лучше неявного
-
-// Отменяет заказ
GET /orders/cancellation
cancellation
(что это?), тем более немодифицирующим методом GET
, чтобы отменить заказ; cancellation
(что это?), тем более немодифицирующим методом GET
, чтобы отменить заказ.// Отменяет заказ
POST /orders/cancel
-
@@ -618,81 +629,65 @@ GET /orders/statistics
// Возвращает агрегированную статистику заказов за всё время
GET /orders/statistics
-
+// Возвращает агрегированную статистику заказов за указанный период
POST /v1/orders/statistics/aggregate
{ "start_date", "end_date" }
-
GET
.2. Использованные стандарты указывайте явно
-
+"date":"11/12/2020"
— стандартов записи дат существует огромное количество, плюс из этой записи невозможно даже понять, что здесь число, а что месяц;
-хорошо: "iso_date":"2020-11-12"
."duration":5000
— пять тысяч чего?
-хорошо:
-"duration_ms":5000
-либо
-"duration":"5000ms"
-либо
-"duration":{"unit":"ms","value":5000}
."date":"11/12/2020"
— стандартов записи дат существует огромное количество, плюс из этой записи невозможно даже понять, что здесь число, а что месяц."iso_date":"2020-11-12"
."duration":5000
— пять тысяч чего?
+ "duration_ms":5000
+ либо
+ "duration":"5000ms"
+ либо
+ "duration":{"unit":"ms","value":5000}
.3. Сохраняйте точность дробных чисел
4. Сущности должны именоваться конкретно
-
-
+user.get()
— неочевидно, что конкретно будет возвращено;
-хорошо: user.get_id()
.user.get()
— неочевидно, что конкретно будет возвращено.user.get_id()
.5. Не экономьте буквы
-
+
+order.time()
— неясно, о каком времени идёт речь: время создания заказа, время готовности заказа, время ожидания заказа?…
-Хорошо: order.get_estimated_delivery_time()
-// возвращает положение первого вхождения в строку str2
+
order.time()
— неясно, о каком времени идёт речь: время создания заказа, время готовности заказа, время ожидания заказа?…order.get_estimated_delivery_time()
// возвращает положение первого вхождения в строку str2
// любого символа из строки str2
strpbrk (str1, str2)
-
-Возможно, автору этого API казалось, что аббревиатура pbrk
что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк str1
, str2
является набором символов для поиска.
-Хорошо: str_search_for_characters (lookup_character_set, str)
-Однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение string
до str
выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.pbrk
что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк str1
, str2
является набором символов для поиска.str_search_for_characters (lookup_character_set, str)
+— однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение string
до str
выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.6. Тип поля должен быть ясен из его названия
recipe
— мы ожидаем, что его значением является сущность типа Recipe
. Если поле называется recipe_id
— мы ожидаем, что его значением является идентификатор, который мы сможем найти в составе сущности Recipe
.objects
, children
; если это невозможно (термин неисчисляемый), следует добавить префикс или постфикс, не оставляющий сомнений:
-
-GET /news
— неясно, будет ли получена какая-то конкретная новость или массив новостей;
-хорошо: GET /news-list
.is_ready
, open_now
:
-
+"task.status": true
— неочевидно, что статус бинарен, плюс такое API будет нерасширяемым;
-хорошо: "task.is_finished": true
.objects
, children
; если это невозможно (термин неисчисляемый), следует добавить префикс или постфикс, не оставляющий сомнений.GET /news
— неясно, будет ли получена какая-то конкретная новость или массив новостей.GET /news-list
.is_ready
, open_now
."task.status": true
— неочевидно, что статус бинарен, плюс такое API будет нерасширяемым."task.is_finished": true
.Date
, если таковые имеются, разумно индицировать с помощью, например, постфикса _at
(created_at
, occurred_at
, etc) или _date
.
-
+
-// Возвращает список встроенных функций кофе-машины
-GET /coffee-machines/functions
-
-Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).
-Хорошо: GET /v1/coffee-machines/builtin-functions-list
+// Возвращает список встроенных функций кофе-машины
+GET /coffee-machines/{id}/functions
+
GET /v1/coffee-machines/{id}/builtin-functions-list
7. Подобные сущности должны называться подобно и вести себя подобным образом
-
-
+begin_transition
/ stop_transition
-— begin
и stop
— непарные термины; разработчик будет вынужден рыться в документации.
-Хорошо: begin_transition
/ end_transition
либо start_transition
/ stop_transition
.begin_transition
/ stop_transition
+— begin
и stop
— непарные термины; разработчик будет вынужден рыться в документации.begin_transition
/ end_transition
либо start_transition
/ stop_transition
.// Находит первую позицию позицию строки `needle`
// внутри строки `haystack`
strpos(haystack, needle)
@@ -705,22 +700,19 @@ str_replace(needle, replace, haystack)
-needle
/haystack
; needle
, а другой — все вхождения, и об этом поведении никак нельзя узнать из сигнатуры функций.needle
, а другой — все вхождения, и об этом поведении никак нельзя узнать из сигнатуры функций.8. Клиент всегда должен знать полное состояние системы
-
+
+
-// Создаёт комментарий и возвращает его id
+
+// Создаёт комментарий и возвращает его id
POST /comments
{ "content" }
→
{ "comment_id" }
-
-
-// Возвращает комментарий по его id
+
+// Возвращает комментарий по его id
GET /comments/{id}
→
{
@@ -730,64 +722,54 @@ GET /comments/{id}
"action_required": "solve_captcha",
"content"
}
-
-— хотя операция будто бы выполнена успешна, клиенту необходимо сделать дополнительный запрос, чтобы понять необходимость решения капчи. Между вызовами POST /comments
и GET /comments/{id}
клиент находится в состоянии кота Шрёдингера: непонятно, опубликован комментарий или нет, и как отразить это пользователю.
-Хорошо:
-
-// Создаёт комментарий и возвращает его
+
POST /comments
и GET /comments/{id}
клиент находится в состоянии кота Шрёдингера: непонятно, опубликован комментарий или нет, и как отразить это пользователю.
+// Создаёт комментарий и возвращает его
POST /v1/comments
{ "content" }
→
{ "comment_id", "published", "action_required", "content" }
-
-
-// Возвращает комментарий по его id
+
// Возвращает комментарий по его id
GET /v1/comments/{id}
→
{ /* в точности тот же формат,
- что и в ответе POST /comments */
- …
+ что и в ответе POST /comments */
+ …
}
-
-Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа невелик) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.9. Идемпотентность
-
-
+
+
-// Создаёт заказ
+
// Создаёт заказ
POST /orders
-
-Повтор запроса создаст два заказа!
-// Создаёт заказ
+
+
+// Создаёт заказ
POST /v1/orders
X-Idempotency-Token: <случайная строка>
-
-Клиент на своей стороне запоминает X-Idempotency-Token, и, в случае автоматического повторного перезапроса, обязан его сохранить. Сервер на своей стороне проверяет токен и, если заказ с таким токеном уже существует для этого клиента, не даёт создать заказ повторно.
-Альтернатива:
-
-// Создаёт черновик заказа
+
+// Создаёт черновик заказа
POST /v1/orders
→
{ "draft_id" }
-
-
-// Подтверждает черновик заказа
+
// Подтверждает черновик заказа
PUT /v1/orders/drafts/{draft_id}
{ "confirmed": true }
-
-Создание черновика заказа — необязывающая операция, которая не приводит ни к каким последствиям, поэтому допустимо создавать черновики без токена идемпотентности.
-Операция подтверждения заказа — уже естественным образом идемпотентна, для неё draft_id
играет роль ключа идемпотентности.draft_id
играет роль ключа идемпотентности.10. Кэширование
-
+// Возвращает цену лунго в кафе,
// ближайшем к указанной точке
GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
@@ -797,8 +779,10 @@ GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
-
-
+
// Возвращает предложение: за какую сумму
// наш сервис готов приготовить лунго
GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
@@ -819,13 +803,11 @@ GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
}
}
}
-
11. Пагинация, фильтрация и курсоры
-
// Возвращает указанный limit записей,
// отсортированных по дате создания
// начиная с записи с номером offset
@@ -843,7 +825,8 @@ GET /records?limit=10&offset=100
Произойдёт следующее: клиент пропустит одну запись и никогда не сможет об этом узнать.
-Никакие: повторяя запрос с теми же limit-offset мы каждый раз получаем новый набор записей.// Возвращает указанный limit записей,
// отсортированных по дате создания,
@@ -858,8 +841,8 @@ GET /records?newer_than={record_id}&limit=10
-Другой вариант организации таких списков — возврат курсора cursor
, который используется вместо record_id
, что делает интерфейсы универсальнее.cursor
, который используется вместо record_id
, что делает интерфейсы универсальнее.
Плохо:
// Возвращает указанный limit записей,
// отсортированных по полю sort_by
// в порядке sort_order,
@@ -898,6 +881,6 @@ GET /v1/record-views/{id}?cursor={cursor}
"cursor"
}
-Недостатком этой схемы является необходимость заводить отдельные списки под каждый вид сортировки.
+Недостатком этой схемы является необходимость заводить отдельные списки под каждый вид сортировки, а также появление множества событий для одной записи, если данные меняются часто.