1
0
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:
Sergey Konstantinov 2020-12-05 01:06:43 +03:00
parent 6dfa58adbd
commit 769dc1dada
6 changed files with 403 additions and 387 deletions

View File

@ -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: &lt;случайная строка&gt;
</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&amp;longitude={longitude}&amp;latitude={latitude}
@ -797,8 +779,10 @@ GET /price?recipe=lungo&amp;longitude={longitude}&amp;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&amp;longitude={longitude}&amp;latitude={latitude}
@ -819,13 +803,11 @@ GET /price?recipe=lungo&amp;longitude={longitude}&amp;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&amp;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}&amp;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>

Binary file not shown.

View File

@ -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"

View File

@ -25,7 +25,9 @@
GET /v1/recipes/lungo
```
```
// размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа
// размещает на указанной кофе-машине
// заказ на приготовление лунго
// и возвращает идентификатор заказа
POST /v1/coffee-machines/orders?machine_id={id}
{
"recipe": "lungo"

View File

@ -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"
}
```
Недостатком этой схемы является необходимость заводить отдельные списки под каждый вид сортировки.
Недостатком этой схемы является необходимость заводить отдельные списки под каждый вид сортировки, а также появление множества событий для одной записи, если данные меняются часто.

View File

@ -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 {