mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-01-23 17:53:04 +02:00
style fix
This commit is contained in:
parent
6dfa58adbd
commit
769dc1dada
231
docs/API.ru.html
231
docs/API.ru.html
@ -18,7 +18,10 @@
|
||||
|
||||
@media print {
|
||||
h1 {
|
||||
margin: 3.5in 0 4in 0;
|
||||
margin: 4.5in 0 5.2in 0;
|
||||
}
|
||||
body {
|
||||
font-size: 20pt;
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,13 +32,15 @@
|
||||
|
||||
code, pre {
|
||||
font-family: Inconsolata, sans-serif;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 12pt 0;
|
||||
padding: 12pt;
|
||||
margin: 1em 0;
|
||||
padding: 1em;
|
||||
border-radius: .25em;
|
||||
border-top: 1px solid rgba(0,0,0,.45);
|
||||
border-left: 1px solid rgba(0,0,0,.45);
|
||||
@ -43,6 +48,10 @@ pre {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
pre code {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.page-break {
|
||||
page-break-after: always;
|
||||
}
|
||||
@ -55,22 +64,23 @@ h1, h2, h3, h4, h5 {
|
||||
text-align: left;
|
||||
font-family: 'PT Sans';
|
||||
font-weight: bold;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28pt;
|
||||
font-size: 200%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24pt;
|
||||
font-size: 160%;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 20pt;
|
||||
font-size: 140%;
|
||||
}
|
||||
|
||||
h4, h5 {
|
||||
font-size: 16pt;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
@page {
|
||||
@ -227,7 +237,9 @@ POST /v1/bucket/{id}/some-resource
|
||||
<pre><code> // возвращает рецепт лунго
|
||||
GET /v1/recipes/lungo
|
||||
</code></pre>
|
||||
<pre><code> // размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа
|
||||
<pre><code> // размещает на указанной кофе-машине
|
||||
// заказ на приготовление лунго
|
||||
// и возвращает идентификатор заказа
|
||||
POST /v1/coffee-machines/orders?machine_id={id}
|
||||
{
|
||||
"recipe": "lungo"
|
||||
@ -599,17 +611,16 @@ GET /sensors
|
||||
<p>Важно понимать, что вы вольны вводить свои собственные конвенции. Например, в некоторых фреймворках сознательно отказываются от парных методов <code>set_entity</code> / <code>get_entity</code> в пользу одного метода <code>entity</code> с опциональным параметром. Важно только проявить последовательность в её применении — если такая конвенция вводится, то абсолютно все методы API должны иметь подобную полиморфную сигнатуру, или по крайней мере должен существовать принцип именования, отличающий такие комбинированные методы от обычных вызовов.</p>
|
||||
<h4 id="1">1. Явное лучше неявного</h4>
|
||||
<p>Из названия любой сущности должно быть очевидно, что она делает и к каким сайд-эффектам может привести её использование.</p>
|
||||
<ul>
|
||||
<li><p><strong>Плохо</strong>: </p>
|
||||
<p><strong>Плохо</strong>: </p>
|
||||
<pre><code>// Отменяет заказ
|
||||
GET /orders/cancellation
|
||||
</code></pre>
|
||||
<p>Неочевидно, что достаточно просто обращения к сущности <code>cancellation</code> (что это?), тем более немодифицирующим методом <code>GET</code>, чтобы отменить заказ; </p>
|
||||
<p>Неочевидно, что достаточно просто обращения к сущности <code>cancellation</code> (что это?), тем более немодифицирующим методом <code>GET</code>, чтобы отменить заказ.</p>
|
||||
<p><strong>Хорошо</strong>: </p>
|
||||
<pre><code>// Отменяет заказ
|
||||
POST /orders/cancel
|
||||
</code></pre></li>
|
||||
<li><p><strong>Плохо</strong>:</p>
|
||||
</code></pre>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Возвращает агрегированную статистику заказов за всё время
|
||||
GET /orders/statistics
|
||||
</code></pre>
|
||||
@ -618,81 +629,65 @@ GET /orders/statistics
|
||||
<pre><code>// Возвращает агрегированную статистику заказов за указанный период
|
||||
POST /v1/orders/statistics/aggregate
|
||||
{ "start_date", "end_date" }
|
||||
</code></pre></li>
|
||||
</ul>
|
||||
</code></pre>
|
||||
<p><strong>Стремитесь к тому, чтобы из сигнатуры функции было абсолютно ясно, что она делает, что принимает на вход и что возвращает</strong>. Вообще, при прочтении кода, работающего с вашим API, должно быть сразу понятно, что, собственно, он делает — без подглядывания в документацию.</p>
|
||||
<p>Два важных следствия:</p>
|
||||
<p><strong>1.1.</strong> Если операция модифицирующая, это должно быть очевидно из сигнатуры. В частности, не может быть модифицирующих операций за <code>GET</code>.</p>
|
||||
<p><strong>1.2.</strong> Если в номенклатуре вашего API есть как синхронные операции, так и асинхронные, то (а)синхронность должна быть очевидна из сигнатур, <strong>либо</strong> должна существовать конвенция именования, позволяющая отличать синхронные операции от асинхронных.</p>
|
||||
<h4 id="2">2. Использованные стандарты указывайте явно</h4>
|
||||
<p>К сожалению, человечество не в состоянии договориться о таких простейших вещах, как «с какого дня начинается неделя», что уж говорить о каких-то более сложных стандартах.</p>
|
||||
<p>Поэтому <em>всегда</em> указывайте, по какому конкретно стандарту вы отдаёте те или иные величины. Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе:</p>
|
||||
<ul>
|
||||
<li><strong>плохо</strong>: <code>"date":"11/12/2020"</code> — стандартов записи дат существует огромное количество, плюс из этой записи невозможно даже понять, что здесь число, а что месяц;<br />
|
||||
<strong>хорошо</strong>: <code>"iso_date":"2020-11-12"</code>.</li>
|
||||
<li><strong>плохо</strong>: <code>"duration":5000</code> — пять тысяч чего?<br />
|
||||
<strong>хорошо</strong>:<br />
|
||||
<code>"duration_ms":5000</code><br />
|
||||
либо<br />
|
||||
<code>"duration":"5000ms"</code><br />
|
||||
либо<br />
|
||||
<code>"duration":{"unit":"ms","value":5000}</code>.</li>
|
||||
</ul>
|
||||
<p>Поэтому <em>всегда</em> указывайте, по какому конкретно стандарту вы отдаёте те или иные величины. Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе.</p>
|
||||
<p><strong>Плохо</strong>: <code>"date":"11/12/2020"</code> — стандартов записи дат существует огромное количество, плюс из этой записи невозможно даже понять, что здесь число, а что месяц.</p>
|
||||
<p><strong>Хорошо</strong>: <code>"iso_date":"2020-11-12"</code>.</p>
|
||||
<p><strong>Плохо</strong>: <code>"duration":5000</code> — пять тысяч чего?</p>
|
||||
<p><strong>Хорошо</strong>:<br />
|
||||
<code>"duration_ms":5000</code><br />
|
||||
либо<br />
|
||||
<code>"duration":"5000ms"</code><br />
|
||||
либо<br />
|
||||
<code>"duration":{"unit":"ms","value":5000}</code>.</p>
|
||||
<p>Отдельное следствие из этого правила — денежные величины <em>всегда</em> должны сопровождаться указанием кода валюты.</p>
|
||||
<p>Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что как ни сделай — кто-то останется недовольным. Классический пример такого рода — порядок географических координат ("широта-долгота" против "долгота-широта"). Здесь, увы, есть только один работающий метод борьбы с фрустрацией — «блокнот душевного спокойствия», который будет описан в разделе II.</p>
|
||||
<h4 id="3">3. Сохраняйте точность дробных чисел</h4>
|
||||
<p>Там, где это позволено протоколом, дробные числа с фиксированной запятой — такие, как денежные суммы, например — должны передаваться в виде специально предназначенных для этого объектов, например, Decimal или аналогичных.</p>
|
||||
<p>Если в протоколе нет Decimal-типов (в частности, в JSON нет чисел с фиксированной запятой), следует либо привести к целому (путём домножения на указанный множитель), либо использовать строковый тип.</p>
|
||||
<h4 id="4">4. Сущности должны именоваться конкретно</h4>
|
||||
<p>Избегайте слов-«амёб» без определённой семантики, таких как get, apply, make. Сущности должны именоваться конкретно:</p>
|
||||
<ul>
|
||||
<li><strong>плохо</strong>: <code>user.get()</code> — неочевидно, что конкретно будет возвращено;<br />
|
||||
<strong>хорошо</strong>: <code>user.get_id()</code>.</li>
|
||||
</ul>
|
||||
<p>Избегайте слов-«амёб» без определённой семантики, таких как get, apply, make. Сущности должны именоваться конкретно.</p>
|
||||
<p><strong>Плохо</strong>: <code>user.get()</code> — неочевидно, что конкретно будет возвращено.</p>
|
||||
<p><strong>Хорошо</strong>: <code>user.get_id()</code>.</p>
|
||||
<h4 id="5">5. Не экономьте буквы</h4>
|
||||
<p>В XXI веке давно уже нет нужды называть переменные покороче.</p>
|
||||
<ul>
|
||||
<li><strong>Плохо</strong>: <code>order.time()</code> — неясно, о каком времени идёт речь: время создания заказа, время готовности заказа, время ожидания заказа?…<br />
|
||||
<strong>Хорошо</strong>: <code>order.get_estimated_delivery_time()</code></li>
|
||||
<li><strong>Плохо</strong>:
|
||||
<code>
|
||||
// возвращает положение первого вхождения в строку str2
|
||||
<p><strong>Плохо</strong>: <code>order.time()</code> — неясно, о каком времени идёт речь: время создания заказа, время готовности заказа, время ожидания заказа?…</p>
|
||||
<p><strong>Хорошо</strong>: <code>order.get_estimated_delivery_time()</code></p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// возвращает положение первого вхождения в строку str2
|
||||
// любого символа из строки str2
|
||||
strpbrk (str1, str2)
|
||||
</code>
|
||||
Возможно, автору этого API казалось, что аббревиатура <code>pbrk</code> что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк <code>str1</code>, <code>str2</code> является набором символов для поиска.<br />
|
||||
<strong>Хорошо</strong>: <code>str_search_for_characters (lookup_character_set, str)</code><br />
|
||||
Однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение <code>string</code> до <code>str</code> выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.</li>
|
||||
</ul>
|
||||
</code></pre>
|
||||
<p>Возможно, автору этого API казалось, что аббревиатура <code>pbrk</code> что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк <code>str1</code>, <code>str2</code> является набором символов для поиска.</p>
|
||||
<p><strong>Хорошо</strong>: <code>str_search_for_characters (lookup_character_set, str)</code><br />
|
||||
— однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение <code>string</code> до <code>str</code> выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.</p>
|
||||
<h4 id="6">6. Тип поля должен быть ясен из его названия</h4>
|
||||
<p>Если поле называется <code>recipe</code> — мы ожидаем, что его значением является сущность типа <code>Recipe</code>. Если поле называется <code>recipe_id</code> — мы ожидаем, что его значением является идентификатор, который мы сможем найти в составе сущности <code>Recipe</code>.</p>
|
||||
<p>Сущности-массивы должны именоваться во множественном числе или собирательными выражениями — <code>objects</code>, <code>children</code>; если это невозможно (термин неисчисляемый), следует добавить префикс или постфикс, не оставляющий сомнений:</p>
|
||||
<ul>
|
||||
<li><strong>плохо</strong>: <code>GET /news</code> — неясно, будет ли получена какая-то конкретная новость или массив новостей;
|
||||
<strong>хорошо</strong>: <code>GET /news-list</code>.</li>
|
||||
</ul>
|
||||
<p>Аналогично, если ожидается булево значение, то из названия это должно быть очевидно, т.е. именование должно описывать некоторое качественное состояние, например, <code>is_ready</code>, <code>open_now</code>:</p>
|
||||
<ul>
|
||||
<li><strong>плохо</strong>: <code>"task.status": true</code> — неочевидно, что статус бинарен, плюс такое API будет нерасширяемым;<br />
|
||||
<strong>хорошо</strong>: <code>"task.is_finished": true</code>.</li>
|
||||
</ul>
|
||||
<p>Сущности-массивы должны именоваться во множественном числе или собирательными выражениями — <code>objects</code>, <code>children</code>; если это невозможно (термин неисчисляемый), следует добавить префикс или постфикс, не оставляющий сомнений.</p>
|
||||
<p><strong>Плохо</strong>: <code>GET /news</code> — неясно, будет ли получена какая-то конкретная новость или массив новостей.</p>
|
||||
<p><strong>Хорошо</strong>: <code>GET /news-list</code>.</p>
|
||||
<p>Аналогично, если ожидается булево значение, то из названия это должно быть очевидно, т.е. именование должно описывать некоторое качественное состояние, например, <code>is_ready</code>, <code>open_now</code>.</p>
|
||||
<p><strong>Плохо</strong>: <code>"task.status": true</code> — неочевидно, что статус бинарен, плюс такое API будет нерасширяемым.</p>
|
||||
<p><strong>Хорошо</strong>: <code>"task.is_finished": true</code>.</p>
|
||||
<p>Отдельно следует оговорить, что на разных платформах эти правила следует дополнить по-своему с учетом специфики first-class citizen-типов. Например, объекты типа <code>Date</code>, если таковые имеются, разумно индицировать с помощью, например, постфикса <code>_at</code> (<code>created_at</code>, <code>occurred_at</code>, etc) или <code>_date</code>.</p>
|
||||
<p>Если наименование сущности само по себе является каким-либо термином, способным смутить разработчика, лучше добавить лишний префикс или постфикс, чтобы избежать непонимания.</p>
|
||||
<ul>
|
||||
<li><strong>Плохо</strong>:
|
||||
<code>
|
||||
// Возвращает список встроенных функций кофе-машины
|
||||
GET /coffee-machines/functions
|
||||
</code>
|
||||
Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).<br />
|
||||
<strong>Хорошо</strong>: <code>GET /v1/coffee-machines/builtin-functions-list</code></li>
|
||||
</ul>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Возвращает список встроенных функций кофе-машины
|
||||
GET /coffee-machines/{id}/functions
|
||||
</code></pre>
|
||||
<p>Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).</p>
|
||||
<p><strong>Хорошо</strong>: <code>GET /v1/coffee-machines/{id}/builtin-functions-list</code></p>
|
||||
<h4 id="7">7. Подобные сущности должны называться подобно и вести себя подобным образом</h4>
|
||||
<ul>
|
||||
<li><p><strong>Плохо</strong>: <code>begin_transition</code> / <code>stop_transition</code><br />
|
||||
— <code>begin</code> и <code>stop</code> — непарные термины; разработчик будет вынужден рыться в документации.<br />
|
||||
<strong>Хорошо</strong>: <code>begin_transition</code> / <code>end_transition</code> либо <code>start_transition</code> / <code>stop_transition</code>.</p></li>
|
||||
<li><p><strong>Плохо</strong>: </p>
|
||||
<p><strong>Плохо</strong>: <code>begin_transition</code> / <code>stop_transition</code><br />
|
||||
— <code>begin</code> и <code>stop</code> — непарные термины; разработчик будет вынужден рыться в документации.</p>
|
||||
<p><strong>Хорошо</strong>: <code>begin_transition</code> / <code>end_transition</code> либо <code>start_transition</code> / <code>stop_transition</code>.</p>
|
||||
<p><strong>Плохо</strong>: </p>
|
||||
<pre><code>// Находит первую позицию позицию строки `needle`
|
||||
// внутри строки `haystack`
|
||||
strpos(haystack, needle)
|
||||
@ -705,22 +700,19 @@ str_replace(needle, replace, haystack)
|
||||
<ul>
|
||||
<li>написание неконсистентно в части знака подчеркивания;</li>
|
||||
<li>близкие по смыслу методы имеют разный порядок аргументов <code>needle</code>/<code>haystack</code>; </li>
|
||||
<li>первый из методов находит только первое вхождение строки <code>needle</code>, а другой — все вхождения, и об этом поведении никак нельзя узнать из сигнатуры функций.</li></ul>
|
||||
<p>Упражнение «как сделать эти интерфейсы хорошо» предоставим читателю.</p></li>
|
||||
<li>первый из методов находит только первое вхождение строки <code>needle</code>, а другой — все вхождения, и об этом поведении никак нельзя узнать из сигнатуры функций.</li>
|
||||
</ul>
|
||||
<p>Упражнение «как сделать эти интерфейсы хорошо» предоставим читателю.</p>
|
||||
<h4 id="8">8. Клиент всегда должен знать полное состояние системы</h4>
|
||||
<p>Правило можно ещё сформулировать так: не заставляйте клиент гадать.</p>
|
||||
<ul>
|
||||
<li><strong>Плохо</strong>:
|
||||
<code>
|
||||
// Создаёт комментарий и возвращает его id
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Создаёт комментарий и возвращает его id
|
||||
POST /comments
|
||||
{ "content" }
|
||||
→
|
||||
{ "comment_id" }
|
||||
</code>
|
||||
<code>
|
||||
// Возвращает комментарий по его id
|
||||
</code></pre>
|
||||
<pre><code>// Возвращает комментарий по его id
|
||||
GET /comments/{id}
|
||||
→
|
||||
{
|
||||
@ -730,64 +722,54 @@ GET /comments/{id}
|
||||
"action_required": "solve_captcha",
|
||||
"content"
|
||||
}
|
||||
</code>
|
||||
— хотя операция будто бы выполнена успешна, клиенту необходимо сделать дополнительный запрос, чтобы понять необходимость решения капчи. Между вызовами <code>POST /comments</code> и <code>GET /comments/{id}</code> клиент находится в состоянии кота Шрёдингера: непонятно, опубликован комментарий или нет, и как отразить это пользователю.<br />
|
||||
<strong>Хорошо</strong>:
|
||||
<code>
|
||||
// Создаёт комментарий и возвращает его
|
||||
</code></pre>
|
||||
<p>— хотя операция будто бы выполнена успешна, клиенту необходимо сделать дополнительный запрос, чтобы понять необходимость решения капчи. Между вызовами <code>POST /comments</code> и <code>GET /comments/{id}</code> клиент находится в состоянии кота Шрёдингера: непонятно, опубликован комментарий или нет, и как отразить это пользователю.</p>
|
||||
<p><strong>Хорошо</strong>:</p>
|
||||
<pre><code>// Создаёт комментарий и возвращает его
|
||||
POST /v1/comments
|
||||
{ "content" }
|
||||
→
|
||||
{ "comment_id", "published", "action_required", "content" }
|
||||
</code>
|
||||
<code>
|
||||
// Возвращает комментарий по его id
|
||||
</code></pre>
|
||||
<pre><code>// Возвращает комментарий по его id
|
||||
GET /v1/comments/{id}
|
||||
→
|
||||
{ /* в точности тот же формат,
|
||||
что и в ответе POST /comments */
|
||||
…
|
||||
что и в ответе POST /comments */
|
||||
…
|
||||
}
|
||||
</code>
|
||||
Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа невелик) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.</li>
|
||||
</ul>
|
||||
</code></pre>
|
||||
<p>Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа невелик) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.</p>
|
||||
<h4 id="9">9. Идемпотентность</h4>
|
||||
<p>Все эндпойнты должны быть идемпотентны. Напомним, идемпотентность — это следующее свойство: повторный вызов той же операции с теми же параметрами не изменяет результат. Поскольку мы обсуждаем в первую очередь клиент-серверное взаимодействие, узким местом в котором является ненадежность сетевой составляющей, повтор запроса при обрыве соединения — не исключительная ситуация, а норма жизни.</p>
|
||||
<p>Все операции должны быть идемпотентны. Напомним, идемпотентность — это следующее свойство: повторный вызов той же операции с теми же параметрами не изменяет результат. Поскольку мы обсуждаем в первую очередь клиент-серверное взаимодействие, узким местом в котором является ненадежность сетевой составляющей, повтор запроса при обрыве соединения — не исключительная ситуация, а норма жизни.</p>
|
||||
<p>Там, где идемпотентность не может быть обеспечена естественным образом, необходимо добавить явный параметр — ключ идемпотентности или ревизию.</p>
|
||||
<ul>
|
||||
<li><strong>Плохо</strong>
|
||||
<code>
|
||||
// Создаёт заказ
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Создаёт заказ
|
||||
POST /orders
|
||||
</code>
|
||||
Повтор запроса создаст два заказа!</li>
|
||||
<li><strong>Хорошо</strong>
|
||||
<code>
|
||||
// Создаёт заказ
|
||||
</code></pre>
|
||||
<p>Повтор запроса создаст два заказа!</p>
|
||||
<p><strong>Хорошо</strong>:</p>
|
||||
<pre><code>// Создаёт заказ
|
||||
POST /v1/orders
|
||||
X-Idempotency-Token: <случайная строка>
|
||||
</code>
|
||||
Клиент на своей стороне запоминает X-Idempotency-Token, и, в случае автоматического повторного перезапроса, обязан его сохранить. Сервер на своей стороне проверяет токен и, если заказ с таким токеном уже существует для этого клиента, не даёт создать заказ повторно.<br />
|
||||
Альтернатива:
|
||||
<code>
|
||||
// Создаёт черновик заказа
|
||||
</code></pre>
|
||||
<p>Клиент на своей стороне запоминает X-Idempotency-Token, и, в случае автоматического повторного перезапроса, обязан его сохранить. Сервер на своей стороне проверяет токен и, если заказ с таким токеном уже существует для этого клиента, не даёт создать заказ повторно.</p>
|
||||
<p><strong>Альтернатива</strong>:</p>
|
||||
<pre><code>// Создаёт черновик заказа
|
||||
POST /v1/orders
|
||||
→
|
||||
{ "draft_id" }
|
||||
</code>
|
||||
<code>
|
||||
// Подтверждает черновик заказа
|
||||
</code></pre>
|
||||
<pre><code>// Подтверждает черновик заказа
|
||||
PUT /v1/orders/drafts/{draft_id}
|
||||
{ "confirmed": true }
|
||||
</code>
|
||||
Создание черновика заказа — необязывающая операция, которая не приводит ни к каким последствиям, поэтому допустимо создавать черновики без токена идемпотентности.
|
||||
Операция подтверждения заказа — уже естественным образом идемпотентна, для неё <code>draft_id</code> играет роль ключа идемпотентности.</li>
|
||||
</ul>
|
||||
</code></pre>
|
||||
<p>Создание черновика заказа — необязывающая операция, которая не приводит ни к каким последствиям, поэтому допустимо создавать черновики без токена идемпотентности.
|
||||
Операция подтверждения заказа — уже естественным образом идемпотентна, для неё <code>draft_id</code> играет роль ключа идемпотентности.</p>
|
||||
<h4 id="10">10. Кэширование</h4>
|
||||
<p>В клиент-серверном API, как правило, сеть и ресурс сервера не бесконечны, поэтому кэширование на клиенте результатов операции является стандартным действием.</p>
|
||||
<p>Желательно в такой ситуации внести ясность; если не из сигнатур операций, то хотя бы из документации должно быть понятно, каким образом можно кэшировать результат.</p>
|
||||
<ul>
|
||||
<li><p><strong>Плохо</strong></p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Возвращает цену лунго в кафе,
|
||||
// ближайшем к указанной точке
|
||||
GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
|
||||
@ -797,8 +779,10 @@ GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
|
||||
<p>Возникает два вопроса:</p>
|
||||
<ul>
|
||||
<li>в течение какого времени эта цена действительна?</li>
|
||||
<li>на каком расстоянии от указанной точки цена всё ещё действительна? </li></ul>
|
||||
<p>Если на первый вопрос легко ответить введением стандартных заголовков Cache-Control, то для второго вопроса готовых решений нет. В ситуации, когда кэш нужен и по временной, и по пространственной координате следует поступить примерно так:</p>
|
||||
<li>на каком расстоянии от указанной точки цена всё ещё действительна? </li>
|
||||
</ul>
|
||||
<p><strong>Хорошо</strong>:
|
||||
Для указания времени жизни кэша можно пользоваться стандатрными средствами протокола, например, заголовком Cache-Control. В ситуации, когда кэш нужен и по временной, и по пространственной координате следует поступить примерно так:</p>
|
||||
<pre><code>// Возвращает предложение: за какую сумму
|
||||
// наш сервис готов приготовить лунго
|
||||
GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
|
||||
@ -819,13 +803,11 @@ GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
|
||||
}
|
||||
}
|
||||
}
|
||||
</code></pre></li>
|
||||
</ul>
|
||||
</code></pre>
|
||||
<h4 id="11-1">11. Пагинация, фильтрация и курсоры</h4>
|
||||
<p>Любой эндпойнт, возвращающий массивы данных, должен содержать пагинацию. Никаких исключений в этом правиле быть не может.</p>
|
||||
<p>Любой эндпойнт, возвращающий изменяемые данные постранично, должен обеспечивать возможность эти данные перебрать.</p>
|
||||
<ul>
|
||||
<li><p><strong>Плохо</strong>:</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Возвращает указанный limit записей,
|
||||
// отсортированных по дате создания
|
||||
// начиная с записи с номером offset
|
||||
@ -843,7 +825,8 @@ GET /records?limit=10&offset=100
|
||||
<li>Что произойдёт, если при переборе списка одна из записей в уже перебранной части будет удалена?<br />
|
||||
Произойдёт следующее: клиент пропустит одну запись и никогда не сможет об этом узнать.</li>
|
||||
<li>Какие параметры кэширования мы можем выставить на этот эндпойнт?<br />
|
||||
Никакие: повторяя запрос с теми же limit-offset мы каждый раз получаем новый набор записей.</li></ol>
|
||||
Никакие: повторяя запрос с теми же limit-offset, мы каждый раз получаем новый набор записей.</li>
|
||||
</ol>
|
||||
<p><strong>Хорошо</strong>: в таких однонаправленных списках пагинация должна быть организована по тому ключу, порядок которых фиксирован. Например, вот так:</p>
|
||||
<pre><code>// Возвращает указанный limit записей,
|
||||
// отсортированных по дате создания,
|
||||
@ -858,8 +841,8 @@ GET /records?newer_than={record_id}&limit=10
|
||||
</code></pre>
|
||||
<p>При такой организации клиенту не надо заботиться об удалении или добавлении записей в уже перебранной части списка: он продолжает перебор по идентификатору известной записи — первой известной, если надо получить новые записи; последней известной, если надо продолжить перебор.
|
||||
Если операции удаления записей нет, то такие запросы можно свободно кэшировать — по одному и тому же URL будет всегда возвращаться один и тот же набор записей.<br />
|
||||
Другой вариант организации таких списков — возврат курсора <code>cursor</code>, который используется вместо <code>record_id</code>, что делает интерфейсы универсальнее.</p></li>
|
||||
<li><p><strong>Плохо</strong>:</p>
|
||||
Другой вариант организации таких списков — возврат курсора <code>cursor</code>, который используется вместо <code>record_id</code>, что делает интерфейсы универсальнее.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Возвращает указанный limit записей,
|
||||
// отсортированных по полю sort_by
|
||||
// в порядке sort_order,
|
||||
@ -898,6 +881,6 @@ GET /v1/record-views/{id}?cursor={cursor}
|
||||
"cursor"
|
||||
}
|
||||
</code></pre>
|
||||
<p>Недостатком этой схемы является необходимость заводить отдельные списки под каждый вид сортировки.</p></li></ul></li>
|
||||
<p>Недостатком этой схемы является необходимость заводить отдельные списки под каждый вид сортировки, а также появление множества событий для одной записи, если данные меняются часто.</p></li>
|
||||
</ul><div class="page-break"></div></article>
|
||||
</body></html>
|
BIN
docs/API.ru.pdf
BIN
docs/API.ru.pdf
Binary file not shown.
@ -5,8 +5,8 @@
|
||||
"author": "Sergey Konstantinov <twirl-team@yandex.ru>",
|
||||
"repository": "github.com:twirl/The-API-Book",
|
||||
"devDependencies": {
|
||||
"showdown": "^1.9.1",
|
||||
"puppeteer": "^5.3.1"
|
||||
"puppeteer": "^5.5.0",
|
||||
"showdown": "^1.9.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node build.js"
|
||||
|
@ -25,7 +25,9 @@
|
||||
GET /v1/recipes/lungo
|
||||
```
|
||||
```
|
||||
// размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа
|
||||
// размещает на указанной кофе-машине
|
||||
// заказ на приготовление лунго
|
||||
// и возвращает идентификатор заказа
|
||||
POST /v1/coffee-machines/orders?machine_id={id}
|
||||
{
|
||||
"recipe": "lungo"
|
||||
|
@ -18,31 +18,32 @@
|
||||
|
||||
Из названия любой сущности должно быть очевидно, что она делает и к каким сайд-эффектам может привести её использование.
|
||||
|
||||
* **Плохо**:
|
||||
```
|
||||
// Отменяет заказ
|
||||
GET /orders/cancellation
|
||||
```
|
||||
Неочевидно, что достаточно просто обращения к сущности `cancellation` (что это?), тем более немодифицирующим методом `GET`, чтобы отменить заказ;
|
||||
**Плохо**:
|
||||
```
|
||||
// Отменяет заказ
|
||||
GET /orders/cancellation
|
||||
```
|
||||
Неочевидно, что достаточно просто обращения к сущности `cancellation` (что это?), тем более немодифицирующим методом `GET`, чтобы отменить заказ.
|
||||
|
||||
**Хорошо**:
|
||||
```
|
||||
// Отменяет заказ
|
||||
POST /orders/cancel
|
||||
```
|
||||
* **Плохо**:
|
||||
```
|
||||
// Возвращает агрегированную статистику заказов за всё время
|
||||
GET /orders/statistics
|
||||
```
|
||||
Даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы вызов операции без параметров максимально расходовал ресурсы.
|
||||
**Хорошо**:
|
||||
```
|
||||
// Отменяет заказ
|
||||
POST /orders/cancel
|
||||
```
|
||||
|
||||
**Хорошо**:
|
||||
```
|
||||
// Возвращает агрегированную статистику заказов за указанный период
|
||||
POST /v1/orders/statistics/aggregate
|
||||
{ "start_date", "end_date" }
|
||||
```
|
||||
**Плохо**:
|
||||
```
|
||||
// Возвращает агрегированную статистику заказов за всё время
|
||||
GET /orders/statistics
|
||||
```
|
||||
Даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы вызов операции без параметров максимально расходовал ресурсы.
|
||||
|
||||
**Хорошо**:
|
||||
```
|
||||
// Возвращает агрегированную статистику заказов за указанный период
|
||||
POST /v1/orders/statistics/aggregate
|
||||
{ "start_date", "end_date" }
|
||||
```
|
||||
|
||||
**Стремитесь к тому, чтобы из сигнатуры функции было абсолютно ясно, что она делает, что принимает на вход и что возвращает**. Вообще, при прочтении кода, работающего с вашим API, должно быть сразу понятно, что, собственно, он делает — без подглядывания в документацию.
|
||||
|
||||
@ -56,12 +57,15 @@
|
||||
|
||||
К сожалению, человечество не в состоянии договориться о таких простейших вещах, как «с какого дня начинается неделя», что уж говорить о каких-то более сложных стандартах.
|
||||
|
||||
Поэтому _всегда_ указывайте, по какому конкретно стандарту вы отдаёте те или иные величины. Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе:
|
||||
Поэтому _всегда_ указывайте, по какому конкретно стандарту вы отдаёте те или иные величины. Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе.
|
||||
|
||||
* **плохо**: `"date":"11/12/2020"` — стандартов записи дат существует огромное количество, плюс из этой записи невозможно даже понять, что здесь число, а что месяц;
|
||||
**хорошо**: `"iso_date":"2020-11-12"`.
|
||||
* **плохо**: `"duration":5000` — пять тысяч чего?
|
||||
**хорошо**:
|
||||
**Плохо**: `"date":"11/12/2020"` — стандартов записи дат существует огромное количество, плюс из этой записи невозможно даже понять, что здесь число, а что месяц.
|
||||
|
||||
**Хорошо**: `"iso_date":"2020-11-12"`.
|
||||
|
||||
**Плохо**: `"duration":5000` — пять тысяч чего?
|
||||
|
||||
**Хорошо**:
|
||||
`"duration_ms":5000`
|
||||
либо
|
||||
`"duration":"5000ms"`
|
||||
@ -80,150 +84,165 @@
|
||||
|
||||
#### 4. Сущности должны именоваться конкретно
|
||||
|
||||
Избегайте слов-«амёб» без определённой семантики, таких как get, apply, make. Сущности должны именоваться конкретно:
|
||||
* **плохо**: `user.get()` — неочевидно, что конкретно будет возвращено;
|
||||
**хорошо**: `user.get_id()`.
|
||||
Избегайте слов-«амёб» без определённой семантики, таких как get, apply, make. Сущности должны именоваться конкретно.
|
||||
|
||||
**Плохо**: `user.get()` — неочевидно, что конкретно будет возвращено.
|
||||
|
||||
**Хорошо**: `user.get_id()`.
|
||||
|
||||
#### 5. Не экономьте буквы
|
||||
|
||||
В XXI веке давно уже нет нужды называть переменные покороче.
|
||||
|
||||
* **Плохо**: `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` выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.
|
||||
**Плохо**: `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` выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.
|
||||
|
||||
#### 6. Тип поля должен быть ясен из его названия
|
||||
|
||||
Если поле называется `recipe` — мы ожидаем, что его значением является сущность типа `Recipe`. Если поле называется `recipe_id` — мы ожидаем, что его значением является идентификатор, который мы сможем найти в составе сущности `Recipe`.
|
||||
|
||||
Сущности-массивы должны именоваться во множественном числе или собирательными выражениями — `objects`, `children`; если это невозможно (термин неисчисляемый), следует добавить префикс или постфикс, не оставляющий сомнений:
|
||||
* **плохо**: `GET /news` — неясно, будет ли получена какая-то конкретная новость или массив новостей;
|
||||
**хорошо**: `GET /news-list`.
|
||||
Сущности-массивы должны именоваться во множественном числе или собирательными выражениями — `objects`, `children`; если это невозможно (термин неисчисляемый), следует добавить префикс или постфикс, не оставляющий сомнений.
|
||||
|
||||
Аналогично, если ожидается булево значение, то из названия это должно быть очевидно, т.е. именование должно описывать некоторое качественное состояние, например, `is_ready`, `open_now`:
|
||||
* **плохо**: `"task.status": true` — неочевидно, что статус бинарен, плюс такое API будет нерасширяемым;
|
||||
**хорошо**: `"task.is_finished": true`.
|
||||
**Плохо**: `GET /news` — неясно, будет ли получена какая-то конкретная новость или массив новостей.
|
||||
|
||||
**Хорошо**: `GET /news-list`.
|
||||
|
||||
Аналогично, если ожидается булево значение, то из названия это должно быть очевидно, т.е. именование должно описывать некоторое качественное состояние, например, `is_ready`, `open_now`.
|
||||
|
||||
**Плохо**: `"task.status": true` — неочевидно, что статус бинарен, плюс такое API будет нерасширяемым.
|
||||
|
||||
**Хорошо**: `"task.is_finished": true`.
|
||||
|
||||
Отдельно следует оговорить, что на разных платформах эти правила следует дополнить по-своему с учетом специфики first-class citizen-типов. Например, объекты типа `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
|
||||
```
|
||||
Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).
|
||||
|
||||
**Хорошо**: `GET /v1/coffee-machines/{id}/builtin-functions-list`
|
||||
|
||||
#### 7. Подобные сущности должны называться подобно и вести себя подобным образом
|
||||
|
||||
* **Плохо**: `begin_transition` / `stop_transition`
|
||||
— `begin` и `stop` — непарные термины; разработчик будет вынужден рыться в документации.
|
||||
**Хорошо**: `begin_transition` / `end_transition` либо `start_transition` / `stop_transition`.
|
||||
* **Плохо**:
|
||||
```
|
||||
// Находит первую позицию позицию строки `needle`
|
||||
// внутри строки `haystack`
|
||||
strpos(haystack, needle)
|
||||
```
|
||||
```
|
||||
// Находит и заменяет все вхождения строки `needle`
|
||||
// внутри строки `haystack` на строку `replace`
|
||||
str_replace(needle, replace, haystack)
|
||||
```
|
||||
Здесь нарушены сразу несколько правил:
|
||||
* написание неконсистентно в части знака подчеркивания;
|
||||
* близкие по смыслу методы имеют разный порядок аргументов `needle`/`haystack`;
|
||||
* первый из методов находит только первое вхождение строки `needle`, а другой — все вхождения, и об этом поведении никак нельзя узнать из сигнатуры функций.
|
||||
**Плохо**: `begin_transition` / `stop_transition`
|
||||
— `begin` и `stop` — непарные термины; разработчик будет вынужден рыться в документации.
|
||||
|
||||
Упражнение «как сделать эти интерфейсы хорошо» предоставим читателю.
|
||||
**Хорошо**: `begin_transition` / `end_transition` либо `start_transition` / `stop_transition`.
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
// Находит первую позицию позицию строки `needle`
|
||||
// внутри строки `haystack`
|
||||
strpos(haystack, needle)
|
||||
```
|
||||
```
|
||||
// Находит и заменяет все вхождения строки `needle`
|
||||
// внутри строки `haystack` на строку `replace`
|
||||
str_replace(needle, replace, haystack)
|
||||
```
|
||||
Здесь нарушены сразу несколько правил:
|
||||
* написание неконсистентно в части знака подчеркивания;
|
||||
* близкие по смыслу методы имеют разный порядок аргументов `needle`/`haystack`;
|
||||
* первый из методов находит только первое вхождение строки `needle`, а другой — все вхождения, и об этом поведении никак нельзя узнать из сигнатуры функций.
|
||||
|
||||
Упражнение «как сделать эти интерфейсы хорошо» предоставим читателю.
|
||||
|
||||
#### 8. Клиент всегда должен знать полное состояние системы
|
||||
|
||||
Правило можно ещё сформулировать так: не заставляйте клиент гадать.
|
||||
|
||||
* **Плохо**:
|
||||
```
|
||||
// Создаёт комментарий и возвращает его id
|
||||
POST /comments
|
||||
{ "content" }
|
||||
→
|
||||
{ "comment_id" }
|
||||
```
|
||||
```
|
||||
// Возвращает комментарий по его id
|
||||
GET /comments/{id}
|
||||
→
|
||||
{
|
||||
// Комментарий не опубликован
|
||||
// и ждёт прохождения капчи
|
||||
"published": false,
|
||||
"action_required": "solve_captcha",
|
||||
"content"
|
||||
}
|
||||
```
|
||||
— хотя операция будто бы выполнена успешна, клиенту необходимо сделать дополнительный запрос, чтобы понять необходимость решения капчи. Между вызовами `POST /comments` и `GET /comments/{id}` клиент находится в состоянии кота Шрёдингера: непонятно, опубликован комментарий или нет, и как отразить это пользователю.
|
||||
**Хорошо**:
|
||||
```
|
||||
// Создаёт комментарий и возвращает его
|
||||
POST /v1/comments
|
||||
{ "content" }
|
||||
→
|
||||
{ "comment_id", "published", "action_required", "content" }
|
||||
```
|
||||
```
|
||||
// Возвращает комментарий по его id
|
||||
GET /v1/comments/{id}
|
||||
→
|
||||
{ /* в точности тот же формат,
|
||||
что и в ответе POST /comments */
|
||||
…
|
||||
}
|
||||
```
|
||||
Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа невелик) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.
|
||||
**Плохо**:
|
||||
```
|
||||
// Создаёт комментарий и возвращает его id
|
||||
POST /comments
|
||||
{ "content" }
|
||||
→
|
||||
{ "comment_id" }
|
||||
```
|
||||
```
|
||||
// Возвращает комментарий по его id
|
||||
GET /comments/{id}
|
||||
→
|
||||
{
|
||||
// Комментарий не опубликован
|
||||
// и ждёт прохождения капчи
|
||||
"published": false,
|
||||
"action_required": "solve_captcha",
|
||||
"content"
|
||||
}
|
||||
```
|
||||
— хотя операция будто бы выполнена успешна, клиенту необходимо сделать дополнительный запрос, чтобы понять необходимость решения капчи. Между вызовами `POST /comments` и `GET /comments/{id}` клиент находится в состоянии кота Шрёдингера: непонятно, опубликован комментарий или нет, и как отразить это пользователю.
|
||||
|
||||
**Хорошо**:
|
||||
```
|
||||
// Создаёт комментарий и возвращает его
|
||||
POST /v1/comments
|
||||
{ "content" }
|
||||
→
|
||||
{ "comment_id", "published", "action_required", "content" }
|
||||
```
|
||||
```
|
||||
// Возвращает комментарий по его id
|
||||
GET /v1/comments/{id}
|
||||
→
|
||||
{ /* в точности тот же формат,
|
||||
что и в ответе 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` играет роль ключа идемпотентности.
|
||||
**Плохо**:
|
||||
```
|
||||
// Создаёт заказ
|
||||
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` играет роль ключа идемпотентности.
|
||||
|
||||
#### 10. Кэширование
|
||||
|
||||
@ -231,131 +250,133 @@
|
||||
|
||||
Желательно в такой ситуации внести ясность; если не из сигнатур операций, то хотя бы из документации должно быть понятно, каким образом можно кэшировать результат.
|
||||
|
||||
* **Плохо**
|
||||
```
|
||||
// Возвращает цену лунго в кафе,
|
||||
// ближайшем к указанной точке
|
||||
GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
|
||||
→
|
||||
{ "currency_code", "price" }
|
||||
```
|
||||
Возникает два вопроса:
|
||||
* в течение какого времени эта цена действительна?
|
||||
* на каком расстоянии от указанной точки цена всё ещё действительна?
|
||||
**Плохо**:
|
||||
```
|
||||
// Возвращает цену лунго в кафе,
|
||||
// ближайшем к указанной точке
|
||||
GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
|
||||
→
|
||||
{ "currency_code", "price" }
|
||||
```
|
||||
Возникает два вопроса:
|
||||
* в течение какого времени эта цена действительна?
|
||||
* на каком расстоянии от указанной точки цена всё ещё действительна?
|
||||
|
||||
Если на первый вопрос легко ответить введением стандартных заголовков Cache-Control, то для второго вопроса готовых решений нет. В ситуации, когда кэш нужен и по временной, и по пространственной координате следует поступить примерно так:
|
||||
```
|
||||
// Возвращает предложение: за какую сумму
|
||||
// наш сервис готов приготовить лунго
|
||||
GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
|
||||
→
|
||||
{
|
||||
"offer": {
|
||||
"id",
|
||||
"currency_code",
|
||||
"price",
|
||||
"terms": {
|
||||
// До какого времени валидно предложение
|
||||
"valid_until",
|
||||
// Где валидно предложение:
|
||||
// * город
|
||||
// * географический объект
|
||||
// * …
|
||||
"valid_within"
|
||||
}
|
||||
}
|
||||
**Хорошо**:
|
||||
Для указания времени жизни кэша можно пользоваться стандатрными средствами протокола, например, заголовком Cache-Control. В ситуации, когда кэш нужен и по временной, и по пространственной координате следует поступить примерно так:
|
||||
```
|
||||
// Возвращает предложение: за какую сумму
|
||||
// наш сервис готов приготовить лунго
|
||||
GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
|
||||
→
|
||||
{
|
||||
"offer": {
|
||||
"id",
|
||||
"currency_code",
|
||||
"price",
|
||||
"terms": {
|
||||
// До какого времени валидно предложение
|
||||
"valid_until",
|
||||
// Где валидно предложение:
|
||||
// * город
|
||||
// * географический объект
|
||||
// * …
|
||||
"valid_within"
|
||||
}
|
||||
```
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 11. Пагинация, фильтрация и курсоры
|
||||
|
||||
Любой эндпойнт, возвращающий массивы данных, должен содержать пагинацию. Никаких исключений в этом правиле быть не может.
|
||||
|
||||
Любой эндпойнт, возвращающий изменяемые данные постранично, должен обеспечивать возможность эти данные перебрать.
|
||||
* **Плохо**:
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
// Возвращает указанный limit записей,
|
||||
// отсортированных по дате создания
|
||||
// начиная с записи с номером offset
|
||||
GET /records?limit=10&offset=100
|
||||
```
|
||||
На первый взгляд это самый что ни на есть стандартный способ организации пагинации в API. Однако зададим себе три вопроса:
|
||||
1. Каким образом клиент узнает о появлении новых записей в начале списка?
|
||||
Легко заметить, что клиент может только попытаться повторить первый запрос и сличить идентификаторы с запомненным началом списка. Но что делать, если добавленное количество записей превышает limit? Представим себе ситуацию:
|
||||
* клиент обрабатывает записи в порядке поступления;
|
||||
* произошла какая-то проблема, и накопилось большое количество необработанных записей;
|
||||
* клиент запрашивает новые записи (offset=0), однако не находит на первой странице известных идентификаторов — новых записей накопилось больше, чем limit;
|
||||
* клиент вынужден продолжить перебирать записи (увеличивая offset), пока не доберётся до последней известной ему; всё это время клиент простаивает;
|
||||
* таким образом может сложиться ситуация, когда клиент вообще никогда не обработает всю очередь, т.к. будет занят беспорядочным линейным перебором.
|
||||
2. Что произойдёт, если при переборе списка одна из записей в уже перебранной части будет удалена?
|
||||
Произойдёт следующее: клиент пропустит одну запись и никогда не сможет об этом узнать.
|
||||
3. Какие параметры кэширования мы можем выставить на этот эндпойнт?
|
||||
Никакие: повторяя запрос с теми же limit-offset, мы каждый раз получаем новый набор записей.
|
||||
|
||||
**Хорошо**: в таких однонаправленных списках пагинация должна быть организована по тому ключу, порядок которых фиксирован. Например, вот так:
|
||||
```
|
||||
// Возвращает указанный limit записей,
|
||||
// отсортированных по дате создания,
|
||||
// начиная с первой записи, созданной позднее,
|
||||
// чем запись с указанным id
|
||||
GET /records?older_than={record_id}&limit=10
|
||||
// Возвращает указанный limit записей,
|
||||
// отсортированных по дате создания,
|
||||
// начиная с первой записи, созданной раньше,
|
||||
// чем запись с указанным id
|
||||
GET /records?newer_than={record_id}&limit=10
|
||||
```
|
||||
При такой организации клиенту не надо заботиться об удалении или добавлении записей в уже перебранной части списка: он продолжает перебор по идентификатору известной записи — первой известной, если надо получить новые записи; последней известной, если надо продолжить перебор.
|
||||
Если операции удаления записей нет, то такие запросы можно свободно кэшировать — по одному и тому же URL будет всегда возвращаться один и тот же набор записей.
|
||||
Другой вариант организации таких списков — возврат курсора `cursor`, который используется вместо `record_id`, что делает интерфейсы универсальнее.
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
// Возвращает указанный limit записей,
|
||||
// отсортированных по полю sort_by
|
||||
// в порядке sort_order,
|
||||
// начиная с записи с номером offset
|
||||
GET /records?sort_by=date_modified&sort_order=desc&limit=10&offset=100
|
||||
```
|
||||
Сортировка по дате модификации обычно означает, что данные могут меняться. Иными словами, между запросом первой порции данных и запросом второй порции данных какая-то запись может измениться; она просто пропадёт из перечисления, т.к. автоматически попадает на первую страницу. Клиент никогда не получит те записи, которые менялись во время перебора, и у него даже нет способа узнать о самом факте такого пропуска. Помимо этого отметим, что такое API нерасширяемо — невозможно добавить сортировку по двум или более полям.
|
||||
|
||||
**Хорошо**: в представленной постановке задача, вообще говоря, не решается. Список записей по дате изменения всегда будет непредсказуемо изменяться, поэтому необходимо изменить сам подход к формированию данных, одним из двух способов.
|
||||
* Фиксировать порядок в момент обработки запроса; т.е. сервер формирует полный список и сохраняет его в неизменяемом виде:
|
||||
|
||||
```
|
||||
// Возвращает указанный limit записей,
|
||||
// отсортированных по дате создания
|
||||
// начиная с записи с номером offset
|
||||
GET /records?limit=10&offset=100
|
||||
// Создаёт представление по указанным параметрам
|
||||
POST /v1/record-views
|
||||
{
|
||||
sort_by: [
|
||||
{ "field": "date_modified", "order": "desc" }
|
||||
]
|
||||
}
|
||||
→
|
||||
{ "id", "cursor" }
|
||||
```
|
||||
На первый взгляд это самый что ни на есть стандартный способ организации пагинации в API. Однако зададим себе три вопроса:
|
||||
1. Каким образом клиент узнает о появлении новых записей в начале списка?
|
||||
Легко заметить, что клиент может только попытаться повторить первый запрос и сличить идентификаторы с запомненным началом списка. Но что делать, если добавленное количество записей превышает limit? Представим себе ситуацию:
|
||||
* клиент обрабатывает записи в порядке поступления;
|
||||
* произошла какая-то проблема, и накопилось большое количество необработанных записей;
|
||||
* клиент запрашивает новые записи (offset=0), однако не находит на первой странице известных идентификаторов — новых записей накопилось больше, чем limit;
|
||||
* клиент вынужден продолжить перебирать записи (увеличивая offset), пока не доберётся до последней известной ему; всё это время клиент простаивает;
|
||||
* таким образом может сложиться ситуация, когда клиент вообще никогда не обработает всю очередь, т.к. будет занят беспорядочным линейным перебором.
|
||||
2. Что произойдёт, если при переборе списка одна из записей в уже перебранной части будет удалена?
|
||||
Произойдёт следующее: клиент пропустит одну запись и никогда не сможет об этом узнать.
|
||||
3. Какие параметры кэширования мы можем выставить на этот эндпойнт?
|
||||
Никакие: повторяя запрос с теми же limit-offset мы каждый раз получаем новый набор записей.
|
||||
|
||||
**Хорошо**: в таких однонаправленных списках пагинация должна быть организована по тому ключу, порядок которых фиксирован. Например, вот так:
|
||||
```
|
||||
// Возвращает указанный limit записей,
|
||||
// отсортированных по дате создания,
|
||||
// начиная с первой записи, созданной позднее,
|
||||
// чем запись с указанным id
|
||||
GET /records?older_than={record_id}&limit=10
|
||||
// Возвращает указанный limit записей,
|
||||
// отсортированных по дате создания,
|
||||
// начиная с первой записи, созданной раньше,
|
||||
// чем запись с указанным id
|
||||
GET /records?newer_than={record_id}&limit=10
|
||||
// Позволяет получить часть представления
|
||||
GET /v1/record-views/{id}?cursor={cursor}
|
||||
```
|
||||
При такой организации клиенту не надо заботиться об удалении или добавлении записей в уже перебранной части списка: он продолжает перебор по идентификатору известной записи — первой известной, если надо получить новые записи; последней известной, если надо продолжить перебор.
|
||||
Если операции удаления записей нет, то такие запросы можно свободно кэшировать — по одному и тому же URL будет всегда возвращаться один и тот же набор записей.
|
||||
Другой вариант организации таких списков — возврат курсора `cursor`, который используется вместо `record_id`, что делает интерфейсы универсальнее.
|
||||
|
||||
* **Плохо**:
|
||||
Т.к. созданное представление уже неизменяемо, доступ к нему можно организовать как угодно: через курсор, limit/offest, заголовок Range и т.д. Однако надо иметь в виду, что при переборе таких списков может получиться так, что порядок будет нарушен: записи, изменённые уже после генерации представления, будут находиться не на своих местах (либо быть неактуальны, если запись копируется целиком).
|
||||
|
||||
* Гарантировать строгий неизменяемый порядок записей, например, путём введения понятия события изменения записи:
|
||||
|
||||
```
|
||||
// Возвращает указанный limit записей,
|
||||
// отсортированных по полю sort_by
|
||||
// в порядке sort_order,
|
||||
// начиная с записи с номером offset
|
||||
GET /records?sort_by=date_modified&sort_order=desc&limit=10&offset=100
|
||||
POST /v1/records/modified/list
|
||||
{
|
||||
// Опционально
|
||||
"cursor"
|
||||
}
|
||||
→
|
||||
{
|
||||
"modified": [
|
||||
{ "date", "record_id" }
|
||||
],
|
||||
"cursor"
|
||||
}
|
||||
```
|
||||
Сортировка по дате модификации обычно означает, что данные могут меняться. Иными словами, между запросом первой порции данных и запросом второй порции данных какая-то запись может измениться; она просто пропадёт из перечисления, т.к. автоматически попадает на первую страницу. Клиент никогда не получит те записи, которые менялись во время перебора, и у него даже нет способа узнать о самом факте такого пропуска. Помимо этого отметим, что такое API нерасширяемо — невозможно добавить сортировку по двум или более полям.
|
||||
|
||||
**Хорошо**: в представленной постановке задача, вообще говоря, не решается. Список записей по дате изменения всегда будет непредсказуемо изменяться, поэтому необходимо изменить сам подход к формированию данных, одним из двух способов.
|
||||
* Фиксировать порядок в момент обработки запроса; т.е. сервер формирует полный список и сохраняет его в неизменяемом виде:
|
||||
|
||||
```
|
||||
// Создаёт представление по указанным параметрам
|
||||
POST /v1/record-views
|
||||
{
|
||||
sort_by: [
|
||||
{ "field": "date_modified", "order": "desc" }
|
||||
]
|
||||
}
|
||||
→
|
||||
{ "id", "cursor" }
|
||||
```
|
||||
```
|
||||
// Позволяет получить часть представления
|
||||
GET /v1/record-views/{id}?cursor={cursor}
|
||||
```
|
||||
|
||||
Т.к. созданное представление уже неизменяемо, доступ к нему можно организовать как угодно: через курсор, limit/offest, заголовок Range и т.д. Однако надо иметь в виду, что при переборе таких списков может получиться так, что порядок будет нарушен: записи, изменённые уже после генерации представления, будут находиться не на своих местах (либо быть неактуальны, если запись копируется целиком).
|
||||
|
||||
* Гарантировать строгий неизменяемый порядок записей, например, путём введения понятия события изменения записи:
|
||||
|
||||
```
|
||||
POST /v1/records/modified/list
|
||||
{
|
||||
// Опционально
|
||||
"cursor"
|
||||
}
|
||||
→
|
||||
{
|
||||
"modified": [
|
||||
{ "date", "record_id" }
|
||||
],
|
||||
"cursor"
|
||||
}
|
||||
```
|
||||
|
||||
Недостатком этой схемы является необходимость заводить отдельные списки под каждый вид сортировки.
|
||||
Недостатком этой схемы является необходимость заводить отдельные списки под каждый вид сортировки, а также появление множества событий для одной записи, если данные меняются часто.
|
||||
|
||||
|
@ -13,7 +13,10 @@ body {
|
||||
|
||||
@media print {
|
||||
h1 {
|
||||
margin: 3.5in 0 4in 0;
|
||||
margin: 4.5in 0 5.2in 0;
|
||||
}
|
||||
body {
|
||||
font-size: 20pt;
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,13 +27,15 @@ body {
|
||||
|
||||
code, pre {
|
||||
font-family: Inconsolata, sans-serif;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 12pt 0;
|
||||
padding: 12pt;
|
||||
margin: 1em 0;
|
||||
padding: 1em;
|
||||
border-radius: .25em;
|
||||
border-top: 1px solid rgba(0,0,0,.45);
|
||||
border-left: 1px solid rgba(0,0,0,.45);
|
||||
@ -38,6 +43,10 @@ pre {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
pre code {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.page-break {
|
||||
page-break-after: always;
|
||||
}
|
||||
@ -50,22 +59,23 @@ h1, h2, h3, h4, h5 {
|
||||
text-align: left;
|
||||
font-family: 'PT Sans';
|
||||
font-weight: bold;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28pt;
|
||||
font-size: 200%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24pt;
|
||||
font-size: 160%;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 20pt;
|
||||
font-size: 140%;
|
||||
}
|
||||
|
||||
h4, h5 {
|
||||
font-size: 16pt;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
@page {
|
||||
|
Loading…
x
Reference in New Issue
Block a user