1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-08-10 21:51:42 +02:00

style fix

This commit is contained in:
Sergey Konstantinov
2020-11-23 22:42:04 +03:00
parent 851136ccd8
commit 7c544d9274
5 changed files with 162 additions and 137 deletions

View File

@@ -138,15 +138,16 @@ h4, h5 {
<p>Для составных частей сущности, к сожалению, достаточно нейтрального термина нам придумать не удалось, поэтому мы используем слова «поля» и «методы».</p> <p>Для составных частей сущности, к сожалению, достаточно нейтрального термина нам придумать не удалось, поэтому мы используем слова «поля» и «методы».</p>
<p>Большинство примеров API в общих разделах будут даны в виде JSON-over-HTTP-эндпойтов. Это некоторая условность, которая помогает описать концепции, как нам кажется, максимально понятно. Вместо <code>GET /v1/orders</code> вполне может быть вызов метода <code>orders.get()</code>, локальный или удалённый; вместо JSON может быть любой другой формат данных. Смысл утверждений от этого не меняется.</p> <p>Большинство примеров API в общих разделах будут даны в виде JSON-over-HTTP-эндпойтов. Это некоторая условность, которая помогает описать концепции, как нам кажется, максимально понятно. Вместо <code>GET /v1/orders</code> вполне может быть вызов метода <code>orders.get()</code>, локальный или удалённый; вместо JSON может быть любой другой формат данных. Смысл утверждений от этого не меняется.</p>
<p>Рассмотрим следующую запись:</p> <p>Рассмотрим следующую запись:</p>
<pre><code>POST /v1/bucket/{id}/some-resource <pre><code>// Описание метода
POST /v1/bucket/{id}/some-resource
{ {
// Это однострочный комментарий // Это однострочный комментарий
"some_parameter": "value", "some_parameter": "value",
} }
</code></pre>
<pre><code>{ {
/* А это многострочный /* А это многострочный
комментарий */ комментарий */
"operation_id" "operation_id"
@@ -158,11 +159,9 @@ h4, h5 {
<li>в качестве тела запроса передаётся JSON, содержащий поле <code>some_parameter</code> со значением <code>value</code> и ещё какие-то поля, которые для краткости опущены (что показано многоточием);</li> <li>в качестве тела запроса передаётся JSON, содержащий поле <code>some_parameter</code> со значением <code>value</code> и ещё какие-то поля, которые для краткости опущены (что показано многоточием);</li>
<li>телом ответа является JSON, состоящий из единственного поля <code>operation_id</code>; отсутствие значения поля означает, что его значением является именно то, что в этом поле и ожидается — в данном случае какой-то идентификатор операции.</li> <li>телом ответа является JSON, состоящий из единственного поля <code>operation_id</code>; отсутствие значения поля означает, что его значением является именно то, что в этом поле и ожидается — в данном случае какой-то идентификатор операции.</li>
</ul> </ul>
<p>Для упрощения неважен возможна сокращенная запись вида:</p> <p>Тело ответа или запроса может быть опущено, если в контексте обсуждаемого вопроса его содержание не имеет значения.</p>
<ul> <p>Для упрощения возможна сокращенная запись вида: <code>POST /v1/bucket/{id}/some-resource</code> <code>{…,"some_parameter",…}</code><code>{ "operation_id" }</code>; тело запроса и/или ответа может опускаться аналогично полной записи.</p>
<li><code>POST /v1/bucket/{id}/some-resource</code> <code>{…,"some_parameter",…}</code>если тела ответа нет или оно нам не понадобится в ходе рассмотрения примера.</li> <p>Чтобы сослаться на это описание будут использоваться выражения типа «метод <code>POST /v1/bucket/{id}/some-resource</code>» или, для простоты, «метод <code>some-resource</code>» или «метод <code>bucket/some-resource</code>» (если никаких других <code>some-resource</code> в контексте главы не упоминается и перепутать не с чем).</p><div class="page-break"></div><h2>I. Проектирование API</h2><h3 id="7api">Глава 7. Пирамида контекстов API</h3>
</ul>
<p>Чтобы сослаться на это описание будут использоваться выражения типа «метод <code>POST /v1/bucket/{id}/some-resource</code>» или, для простоты, «метод <code>/some-resource</code>» (если никаких других <code>some-resource</code> в контексте главы не упоминается и перепутать не с чем).</p><div class="page-break"></div><h2>I. Проектирование API</h2><h3 id="7api">Глава 7. Пирамида контекстов API</h3>
<p>Подход, который мы используем для проектирования, состоит из четырёх шагов:</p> <p>Подход, который мы используем для проектирования, состоит из четырёх шагов:</p>
<ul> <ul>
<li>определение области применения;</li> <li>определение области применения;</li>
@@ -214,11 +213,9 @@ h4, h5 {
<p>Прежде чем переходить к теории, следует чётко сформулировать, <em>зачем</em> нужны уровни абстракции и каких целей мы хотим достичь их выделением.</p> <p>Прежде чем переходить к теории, следует чётко сформулировать, <em>зачем</em> нужны уровни абстракции и каких целей мы хотим достичь их выделением.</p>
<p>Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим?</p> <p>Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим?</p>
<ol> <ol>
<li>Непосредственно состояние кофе-машины и шаги приготовления кофе. Температура, давление, объём воды.</li> <li>Мы готовим с помощью нашего API <em>заказ</em> — один или несколько стаканов кофе — и взымаем за это плату.</li>
<li>У кофе есть мета-характеристики: сорт, вкус, вид напитка.</li> <li>Каждый стакан кофе приготовлен по определённому рецепту, что подразумевает наличие разных ингредиентов и последовательности выполнения шагов приготовления.</li>
<li>Мы готовим с помощью нашего API <em>заказ</em> — один или несколько стаканов кофе с определенной стоимостью.</li> <li>Напиток готовится на конкретной физической кофе-машине, располагающейся в какой-то точке пространства.</li>
<li>Наши кофе-машины как-то распределены в пространстве (и времени).</li>
<li>Кофе-машина принадлежит какой-то сети кофеен, каждая из которых обладает какой-то айдентикой и специальными возможностями.</li>
</ol> </ol>
<p>Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы прежде всего стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей:</p> <p>Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы прежде всего стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей:</p>
<ol> <ol>
@@ -227,28 +224,35 @@ h4, h5 {
<li><p>Поддержание интероперабельности. Правильно выделенные низкоуровневые абстракции позволят нам адаптировать наше API к другим платформам, не меняя высокоуровневый интерфейс.</p></li> <li><p>Поддержание интероперабельности. Правильно выделенные низкоуровневые абстракции позволят нам адаптировать наше API к другим платформам, не меняя высокоуровневый интерфейс.</p></li>
</ol> </ol>
<p>Допустим, мы имеем следующий интерфейс:</p> <p>Допустим, мы имеем следующий интерфейс:</p>
<ul> <pre><code> // возвращает рецепт лунго
<li><code>GET /v1/recipes/lungo</code><br /> GET /v1/recipes/lungo
— возвращает рецепт лунго;</li> </code></pre>
<li><code>POST /v1/coffee-machines/orders?machine_id={id}</code><br /> <pre><code> // размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа
<code>{recipe:"lungo"}</code><br /> POST /v1/coffee-machines/orders?machine_id={id}
— размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа;</li> {
<li><code>GET /v1/orders?order_id={id}</code><br /> "recipe": "lungo"
— возвращает состояние заказа;</li> }
</ul> </code></pre>
<pre><code> // возвращает состояние заказа
GET /v1/orders?order_id={id}
</code></pre>
<p>И зададимся вопросом, каким образом разработчик определит, что заказ клиента готов. Допустим, мы сделаем так: добавим в рецепт лунго эталонный объём, а в состояние заказа — количество уже налитого кофе. Тогда разработчику нужно будет проверить совпадение этих двух цифр, чтобы убедиться, что кофе готов.</p> <p>И зададимся вопросом, каким образом разработчик определит, что заказ клиента готов. Допустим, мы сделаем так: добавим в рецепт лунго эталонный объём, а в состояние заказа — количество уже налитого кофе. Тогда разработчику нужно будет проверить совпадение этих двух цифр, чтобы убедиться, что кофе готов.</p>
<p>Такое решение выглядит интуитивно плохим, и это действительно так: оно нарушает все вышеперечисленные принципы:</p> <p>Такое решение выглядит интуитивно плохим, и это действительно так: оно нарушает все вышеперечисленные принципы.</p>
<ol> <ol>
<li><p>Для решения задачи «заказать лунго» разработчику нужно обратиться к сущности «рецепт» и выяснить, что у каждого рецепта есть объём. Далее, нужно принять концепцию, что приготовление кофе заканчивается в тот момент, когда объём сравнялся с эталонным. Нет никакого способа об этой конвенции догадаться: она неочевидна и её нужно найти в документации. При этом никакой пользы для разработчика в этом знании нет.</p></li> <li><p>Для решения задачи «заказать лунго» разработчику нужно обратиться к сущности «рецепт» и выяснить, что у каждого рецепта есть объём. Далее, нужно принять концепцию, что приготовление кофе заканчивается в тот момент, когда объём сравнялся с эталонным. Нет никакого способа об этой конвенции догадаться: она неочевидна и её нужно найти в документации. При этом никакой пользы для разработчика в этом знании нет.</p></li>
<li><p>Мы автоматически получаем проблемы, если захотим варьировать размер кофе. Допустим, в какой-то момент мы захотим представить пользователю выбор, сколько конкретно миллилитров лунго он желает. Тогда нам придётся проделать один из следующих трюков:</p> <li><p>Мы автоматически получаем проблемы, если захотим варьировать размер кофе. Допустим, в какой-то момент мы захотим представить пользователю выбор, сколько конкретно миллилитров лунго он желает. Тогда нам придётся проделать один из следующих трюков:</p>
<ul> <ul>
<li>или мы фиксируем список допустимых объёмов и заводим фиктивные рецепты типа <code>/recipes/small-lungo</code>, <code>recipes/large-lungo</code>. Почему фиктивные? Потому что рецепт один и тот же, меняется только объём. Нам придётся либо тиражировать одинаковые рецепты, отличающиеся только объёмом, либо вводить какое-то «наследование» рецептов, чтобы можно было указать базовый рецепт и только переопределить объём;</li> <li>или мы фиксируем список допустимых объёмов и заводим фиктивные рецепты типа <code>/recipes/small-lungo</code>, <code>recipes/large-lungo</code>. Почему фиктивные? Потому что рецепт один и тот же, меняется только объём. Нам придётся либо тиражировать одинаковые рецепты, отличающиеся только объёмом, либо вводить какое-то «наследование» рецептов, чтобы можно было указать базовый рецепт и только переопределить объём;</li>
<li>или мы модифицируем интерфейс, объявляя объём кофе, указанный в рецепте, значением по умолчанию; при размещении заказа мы разрешаем указать объём, отличный от эталонного:<br /> <li>или мы модифицируем интерфейс, объявляя объём кофе, указанный в рецепте, значением по умолчанию; при размещении заказа мы разрешаем указать объём, отличный от эталонного: </li></ul>
<code>POST /v1/coffee-machines/orders?machine_id={id}</code><br /> <pre><code> POST /v1/coffee-machines/orders?machine_id={id}
<code>{recipe:"lungo","volume":"800ml"}</code><br /> {
Для таких кофе произвольного объёма нужно будет получать требуемый объём не из <code>GET /v1/recipes</code>, а из <code>GET /v1/orders</code>. Сделав так, мы сразу получаем клубок из связанных проблем:</li> "recipe":"lungo",
<li>разработчик, которому придётся поддержать эту функциональность, имеет высокие шансы сделать ошибку: добавив поддержку произвольного объёма кофе в код, работающий с <code>POST /v1/coffee-machines/orders</code> нужно не забыть переписать код проверки готовности заказа;</li> "volume":"800ml"
<li>мы получим классическую ситуацию, когда одно и то же поле (объём кофе) значит разные вещи в разных интерфейсах. В <code>GET /v1/recipes</code> поле «объём» теперь значит «объём, который будет запрошен, если не передать его явно в <code>POST /v1/coffee-machines/orders</code>»; переименовать его в «объём по умолчанию» уже не получится, с этой проблемой теперь придётся жить.</li></ul></li> }
</code></pre>
<p>Для таких кофе произвольного объёма нужно будет получать требуемый объём не из <code>GET /v1/recipes</code>, а из <code>GET /v1/orders</code>. Сделав так, мы сразу получаем клубок из связанных проблем:
* разработчик, которому придётся поддержать эту функциональность, имеет высокие шансы сделать ошибку: добавив поддержку произвольного объёма кофе в код, работающий с <code>POST /v1/coffee-machines/orders</code> нужно не забыть переписать код проверки готовности заказа;
* мы получим классическую ситуацию, когда одно и то же поле (объём кофе) значит разные вещи в разных интерфейсах. В <code>GET /v1/recipes</code> поле «объём» теперь значит «объём, который будет запрошен, если не передать его явно в <code>POST /v1/coffee-machines/orders</code>»; переименовать его в «объём по умолчанию» уже не получится, с этой проблемой теперь придётся жить.</p></li>
<li><p>Вся эта схема полностью неработоспособна, если разные модели кофе-машин производят лунго разного объёма. Для решения задачи «объём лунго зависит от вида машины» нам придётся сделать совсем неприятную вещь: сделать рецепт зависимым от id машины. Тем самым мы начнём активно смешивать уровни абстракции: одной частью нашего API (рецептов) станет невозможно пользоваться без другой части (информации о кофе-машинах). Что немаловажно, от разработчиков потребуется изменить логику своего приложения: если раньше они могли предлагать сначала выбрать объём, а потом кофе-машину, то теперь им придётся полностью изменить этот шаг.</p></li> <li><p>Вся эта схема полностью неработоспособна, если разные модели кофе-машин производят лунго разного объёма. Для решения задачи «объём лунго зависит от вида машины» нам придётся сделать совсем неприятную вещь: сделать рецепт зависимым от id машины. Тем самым мы начнём активно смешивать уровни абстракции: одной частью нашего API (рецептов) станет невозможно пользоваться без другой части (информации о кофе-машинах). Что немаловажно, от разработчиков потребуется изменить логику своего приложения: если раньше они могли предлагать сначала выбрать объём, а потом кофе-машину, то теперь им придётся полностью изменить этот шаг.</p></li>
</ol> </ol>
<p>Хорошо, допустим, мы поняли, как сделать плохо. Но как же тогда сделать <em>хорошо</em>? Разделение уровней абстракции должно происходить вдоль трёх направлений:</p> <p>Хорошо, допустим, мы поняли, как сделать плохо. Но как же тогда сделать <em>хорошо</em>? Разделение уровней абстракции должно происходить вдоль трёх направлений:</p>
@@ -290,8 +294,9 @@ h4, h5 {
<p>Предположим, для большей конкретности, что эти два класса устройств поставляются вот с таким физическим API:</p> <p>Предположим, для большей конкретности, что эти два класса устройств поставляются вот с таким физическим API:</p>
<ul> <ul>
<li><p>Машины с предустановленными программами:</p> <li><p>Машины с предустановленными программами:</p>
<pre><code>GET /programs <pre><code>// Возвращает список предустановленных программ
// Возвращает список предустановленных программ GET /programs
{ {
// идентификатор программы // идентификатор программы
"program": "01", "program": "01",
@@ -299,13 +304,14 @@ h4, h5 {
"type": "lungo" "type": "lungo"
} }
</code></pre> </code></pre>
<pre><code>POST /execute <pre><code>// Запускает указанную программу на исполнение
// и возвращает статус исполнения
POST /execute
{ {
"program": 1, "program": 1,
"volume": "200ml" "volume": "200ml"
} }
// Запускает указанную программу на исполнение
// и возвращает статус исполнения
{ {
// Уникальный идентификатор задания // Уникальный идентификатор задания
"execution_id": "01-01", "execution_id": "01-01",
@@ -315,17 +321,18 @@ h4, h5 {
"volume": "200ml" "volume": "200ml"
} }
</code></pre> </code></pre>
<pre><code>POST /cancel <pre><code>// Отменяет текущую программу
// Отменяет текущую программу POST /cancel
</code></pre> </code></pre>
<pre><code>GET /execution/status <pre><code>// Возвращает статус исполнения
// Возвращает статус исполнения
// Формат аналогичен формату ответа `POST /execute` // Формат аналогичен формату ответа `POST /execute`
GET /execution/status
</code></pre> </code></pre>
<p><strong>NB</strong>. На всякий случай отметим, что данное API нарушает множество описанных нами принципов проектирования, начиная с отсутствия версионирования; оно приведено в таком виде по двум причинам: (1) чтобы мы могли показать, как спроектировать API более удачно; (2) скорее всего, в реальной жизни вы получите именно такое API от производителей кофе-машин, и это ещё довольно вменяемый вариант.</p></li> <p><strong>NB</strong>. На всякий случай отметим, что данное API нарушает множество описанных нами принципов проектирования, начиная с отсутствия версионирования; оно приведено в таком виде по двум причинам: (1) чтобы мы могли показать, как спроектировать API более удачно; (2) скорее всего, в реальной жизни вы получите именно такое API от производителей кофе-машин, и это ещё довольно вменяемый вариант.</p></li>
<li><p>Машины с предустановленными функциями:</p> <li><p>Машины с предустановленными функциями:</p>
<pre><code>GET /functions <pre><code>// Возвращает список доступных функций
// Возвращает список доступных функций GET /functions
{ {
"functions": [ "functions": [
{ {
@@ -344,16 +351,17 @@ h4, h5 {
] ]
} }
</code></pre> </code></pre>
<pre><code>POST /functions <pre><code>// Запускает на исполнение функцию
// с передачей указанных значений аргументов
POST /functions
{ {
"type": "set_cup", "type": "set_cup",
"arguments": [{ "name": "volume", "value": "300ml" }] "arguments": [{ "name": "volume", "value": "300ml" }]
} }
// Запускает на исполнение функцию
// с передачей указанных значений аргументов
</code></pre> </code></pre>
<pre><code>GET /sensors <pre><code>// Возвращает статусы датчиков
// Возвращает статусы датчиков GET /sensors
{ {
"sensors": [ "sensors": [
{ {
@@ -394,13 +402,18 @@ h4, h5 {
</ol> </ol>
<p>Что это будет означать практически? Разработчик по-прежнему будет создавать заказ, оперируя только высокоуровневыми терминами:</p> <p>Что это будет означать практически? Разработчик по-прежнему будет создавать заказ, оперируя только высокоуровневыми терминами:</p>
<pre><code>POST /v1/coffee-machines/orders?machine_id={id} <pre><code>POST /v1/coffee-machines/orders?machine_id={id}
{recipe:"lungo","volume":"800ml"} {
"recipe": "lungo",
"volume": "800ml"
}
{ "order_id" }
</code></pre> </code></pre>
<p>Имплементация функции <code>POST /orders</code> проверит все параметры заказа, заблокирует его стоимость на карте пользователя, сформирует полный запрос на исполнение и обратится к уровню исполнения. Сначала необходимо подобрать правильную программу исполнения:</p> <p>Имплементация функции <code>POST /orders</code> проверит все параметры заказа, заблокирует его стоимость на карте пользователя, сформирует полный запрос на исполнение и обратится к уровню исполнения. Сначала необходимо подобрать правильную программу исполнения:</p>
<pre><code>POST /v1/programs/match <pre><code>POST /v1/programs/match
{ "recipe", "coffee-machine" } { "recipe", "coffee-machine" }
</code></pre>
<pre><code>{ "program_id" } { "program_id" }
</code></pre> </code></pre>
<p>Получив идентификатор программы, нужно запустить её на исполнение:</p> <p>Получив идентификатор программы, нужно запустить её на исполнение:</p>
<pre><code>POST /v1/programs/{id}/run <pre><code>POST /v1/programs/{id}/run
@@ -414,8 +427,8 @@ h4, h5 {
} }
] ]
} }
</code></pre>
<pre><code>{ "program_run_id" } { "program_run_id" }
</code></pre> </code></pre>
<p>Обратите внимание, что во всей этой цепочке вообще никак не участвует тип API кофе-машины — собственно, ровно для этого мы и абстрагировали. Мы могли бы сделать интерфейсы более конкретными, разделив функциональность <code>run</code> и <code>match</code> для разных API, т.е. ввести раздельные endpoint-ы:</p> <p>Обратите внимание, что во всей этой цепочке вообще никак не участвует тип API кофе-машины — собственно, ровно для этого мы и абстрагировали. Мы могли бы сделать интерфейсы более конкретными, разделив функциональность <code>run</code> и <code>match</code> для разных API, т.е. ввести раздельные endpoint-ы:</p>
<ul> <ul>
@@ -430,8 +443,8 @@ h4, h5 {
<p>Уровень рантаймов API второго типа, исходя из общих соображений, будет скорее всего непубличным, и мы плюс-минус свободны в его имплементации. Самым простым решением будет реализовать виртуальную state-машину, которая создаёт «рантайм» (т.е. stateful контекст исполнения) для выполнения программы и следит за его состоянием.</p> <p>Уровень рантаймов API второго типа, исходя из общих соображений, будет скорее всего непубличным, и мы плюс-минус свободны в его имплементации. Самым простым решением будет реализовать виртуальную state-машину, которая создаёт «рантайм» (т.е. stateful контекст исполнения) для выполнения программы и следит за его состоянием.</p>
<pre><code>POST /v1/runtimes <pre><code>POST /v1/runtimes
{ "coffee_machine", "program", "parameters" } { "coffee_machine", "program", "parameters" }
</code></pre>
<pre><code>{ "runtime_id", "state" } { "runtime_id", "state" }
</code></pre> </code></pre>
<p>Здесь <code>program</code> будет выглядеть примерно так:</p> <p>Здесь <code>program</code> будет выглядеть примерно так:</p>
<pre><code>{ <pre><code>{
@@ -575,23 +588,23 @@ h4, h5 {
<p>Из названия любой сущности должно быть очевидно, что она делает и к каким сайд-эффектам может привести её использование.</p> <p>Из названия любой сущности должно быть очевидно, что она делает и к каким сайд-эффектам может привести её использование.</p>
<ul> <ul>
<li><p><strong>Плохо</strong>: </p> <li><p><strong>Плохо</strong>: </p>
<pre><code>GET /orders/cancellation <pre><code>// Отменяет заказ
// отменяет заказ GET /orders/cancellation
</code></pre> </code></pre>
<p>Неочевидно, что достаточно просто обращения к сущности <code>cancellation</code> (что это?), тем более немодифицирующим методом <code>GET</code>, чтобы отменить заказ; </p> <p>Неочевидно, что достаточно просто обращения к сущности <code>cancellation</code> (что это?), тем более немодифицирующим методом <code>GET</code>, чтобы отменить заказ; </p>
<p><strong>Хорошо</strong>: </p> <p><strong>Хорошо</strong>: </p>
<pre><code>POST /orders/cancel <pre><code>// Отменяет заказ
отменяет заказ POST /orders/cancel
</code></pre></li> </code></pre></li>
<li><p><strong>Плохо</strong>:</p> <li><p><strong>Плохо</strong>:</p>
<pre><code>GET /orders/statistics <pre><code>// Возвращает агрегированную статистику заказов за всё время
// Возвращает агрегированную статистику заказов за всё время GET /orders/statistics
</code></pre> </code></pre>
<p>Даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы вызов операции без параметров максимально расходовал ресурсы.</p> <p>Даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы вызов операции без параметров максимально расходовал ресурсы.</p>
<p><strong>Хорошо</strong>:</p> <p><strong>Хорошо</strong>:</p>
<pre><code>POST /orders/statistics/aggregate <pre><code>// Возвращает агрегированную статистику заказов за указанный период
POST /orders/statistics/aggregate
{ "start_date", "end_date" } { "start_date", "end_date" }
// Возвращает агрегированную статистику заказов за указанный период
</code></pre></li> </code></pre></li>
</ul> </ul>
<p><strong>Стремитесь к тому, чтобы из сигнатуры функции было абсолютно ясно, что она делает, что принимает на вход и что возвращает</strong>. Вообще, при прочтении кода, работающего с вашим API, должно быть сразу понятно, что, собственно, он делает — без подглядывания в документацию.</p> <p><strong>Стремитесь к тому, чтобы из сигнатуры функции было абсолютно ясно, что она делает, что принимает на вход и что возвращает</strong>. Вообще, при прочтении кода, работающего с вашим API, должно быть сразу понятно, что, собственно, он делает — без подглядывания в документацию.</p>
@@ -630,9 +643,9 @@ h4, h5 {
<strong>Хорошо</strong>: <code>order.get_estimated_delivery_time()</code></li> <strong>Хорошо</strong>: <code>order.get_estimated_delivery_time()</code></li>
<li><strong>Плохо</strong>: <li><strong>Плохо</strong>:
<code> <code>
strpbrk (str1, str2)
// возвращает положение первого вхождения в строку str2 // возвращает положение первого вхождения в строку str2
// любого символа из строки str2 // любого символа из строки str2
strpbrk (str1, str2)
</code> </code>
Возможно, автору этого API казалось, что аббревиатура <code>pbrk</code> что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк <code>str1</code>, <code>str2</code> является набором символов для поиска.<br /> Возможно, автору этого API казалось, что аббревиатура <code>pbrk</code> что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк <code>str1</code>, <code>str2</code> является набором символов для поиска.<br />
<strong>Хорошо</strong>: <code>str_search_for_characters(lookup_character_set, str)</code><br /> <strong>Хорошо</strong>: <code>str_search_for_characters(lookup_character_set, str)</code><br />
@@ -655,8 +668,8 @@ strpbrk (str1, str2)
<ul> <ul>
<li><strong>Плохо</strong>: <li><strong>Плохо</strong>:
<code> <code>
GET /coffee-machines/functions
// Возвращает список встроенных функций кофе-машины // Возвращает список встроенных функций кофе-машины
GET /coffee-machines/functions
</code> </code>
Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).<br /> Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).<br />
<strong>Хорошо</strong>: <code>GET /coffee-machines/builtin-functions-list</code></li> <strong>Хорошо</strong>: <code>GET /coffee-machines/builtin-functions-list</code></li>
@@ -667,13 +680,13 @@ GET /coffee-machines/functions
<code>begin</code> и <code>stop</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> <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> <li><p><strong>Плохо</strong>: </p>
<pre><code>strpos(haystack, needle) <pre><code>// Находит первую позицию позицию строки `needle`
// Находит первую позицию позицию строки `needle`
// внутри строки `haystack` // внутри строки `haystack`
strpos(haystack, needle)
</code></pre> </code></pre>
<pre><code>str_replace(needle, replace, haystack) <pre><code>// Находит и заменяет все вхождения строки `needle`
// Находит и заменяет все вхождения строки `needle`
// внутри строки `haystack` на строку `replace` // внутри строки `haystack` на строку `replace`
str_replace(needle, replace, haystack)
</code></pre> </code></pre>
<p>Здесь нарушены сразу несколько правил:</p> <p>Здесь нарушены сразу несколько правил:</p>
<ul> <ul>
@@ -687,18 +700,16 @@ GET /coffee-machines/functions
<ul> <ul>
<li><strong>Плохо</strong>: <li><strong>Плохо</strong>:
<code> <code>
// Создаёт комментарий и возвращает его id
POST /comments POST /comments
{ "content" } { "content" }
// Создаёт комментарий и возвращает его id
</code>
<code>
{ "comment_id" } { "comment_id" }
</code> </code>
<code> <code>
GET /comments/{id}
// Возвращает комментарий по его id // Возвращает комментарий по его id
</code> GET /comments/{id}
<code>
{ {
// Комментарий не опубликован // Комментарий не опубликован
// и ждёт прохождения капчи // и ждёт прохождения капчи
@@ -710,20 +721,19 @@ GET /comments/{id}
— хотя операция будто бы выполнена успешна, клиенту необходимо сделать дополнительный запрос, чтобы понять необходимость решения капчи. Между вызовами <code>POST /comments</code> и <code>GET /comments/{id}</code> клиент находится в состоянии кота Шрёдингера: непонятно, опубликован комментарий или нет, и как отразить это пользователю.<br /> — хотя операция будто бы выполнена успешна, клиенту необходимо сделать дополнительный запрос, чтобы понять необходимость решения капчи. Между вызовами <code>POST /comments</code> и <code>GET /comments/{id}</code> клиент находится в состоянии кота Шрёдингера: непонятно, опубликован комментарий или нет, и как отразить это пользователю.<br />
<strong>Хорошо</strong>: <strong>Хорошо</strong>:
<code> <code>
// Создаёт комментарий и возвращает его
POST /comments POST /comments
{ "content" } { "content" }
// Создаёт комментарий и возвращает его
</code>
<code>
{ "comment_id", "published", "action_required", "content" } { "comment_id", "published", "action_required", "content" }
</code> </code>
<code> <code>
GET /comments/{id}
// Возвращает комментарий по его id // Возвращает комментарий по его id
</code> GET /comments/{id}
<code>
{ /* в точности тот же формат, { /* в точности тот же формат,
что и в ответе POST /comments */ что и в ответе POST /comments */
} }
</code> </code>
Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа невелик) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.</li> Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа невелик) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.</li>

Binary file not shown.

View File

@@ -8,6 +8,7 @@
Рассмотрим следующую запись: Рассмотрим следующую запись:
``` ```
// Описание метода
POST /v1/bucket/{id}/some-resource POST /v1/bucket/{id}/some-resource
{ {
@@ -15,8 +16,7 @@ POST /v1/bucket/{id}/some-resource
"some_parameter": "value", "some_parameter": "value",
} }
```
```
{ {
/* А это многострочный /* А это многострочный
комментарий */ комментарий */
@@ -29,7 +29,8 @@ POST /v1/bucket/{id}/some-resource
* в качестве тела запроса передаётся JSON, содержащий поле `some_parameter` со значением `value` и ещё какие-то поля, которые для краткости опущены (что показано многоточием); * в качестве тела запроса передаётся JSON, содержащий поле `some_parameter` со значением `value` и ещё какие-то поля, которые для краткости опущены (что показано многоточием);
* телом ответа является JSON, состоящий из единственного поля `operation_id`; отсутствие значения поля означает, что его значением является именно то, что в этом поле и ожидается — в данном случае какой-то идентификатор операции. * телом ответа является JSON, состоящий из единственного поля `operation_id`; отсутствие значения поля означает, что его значением является именно то, что в этом поле и ожидается — в данном случае какой-то идентификатор операции.
Для упрощения неважен возможна сокращенная запись вида: Тело ответа или запроса может быть опущено, если в контексте обсуждаемого вопроса его содержание не имеет значения.
* `POST /v1/bucket/{id}/some-resource` `{…,"some_parameter",…}` — если тела ответа нет или оно нам не понадобится в ходе рассмотрения примера.
Чтобы сослаться на это описание будут использоваться выражения типа «метод `POST /v1/bucket/{id}/some-resource`» или, для простоты, «метод `/some-resource`» (если никаких других `some-resource` в контексте главы не упоминается и перепутать не с чем). Для упрощения возможна сокращенная запись вида: `POST /v1/bucket/{id}/some-resource` `{…,"some_parameter",…}``{ "operation_id" }`; тело запроса и/или ответа может опускаться аналогично полной записи.
Чтобы сослаться на это описание будут использоваться выражения типа «метод `POST /v1/bucket/{id}/some-resource`» или, для простоты, «метод `some-resource`» или «метод `bucket/some-resource`» (если никаких других `some-resource` в контексте главы не упоминается и перепутать не с чем).

View File

@@ -6,11 +6,9 @@
Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим? Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим?
1. Непосредственно состояние кофе-машины и шаги приготовления кофе. Температура, давление, объём воды. 1. Мы готовим с помощью нашего API *заказ* — один или несколько стаканов кофе — и взымаем за это плату.
2. У кофе есть мета-характеристики: сорт, вкус, вид напитка. 2. Каждый стакан кофе приготовлен по определённому рецепту, что подразумевает наличие разных ингредиентов и последовательности выполнения шагов приготовления.
3. Мы готовим с помощью нашего API *заказ* — один или несколько стаканов кофе с определенной стоимостью. 3. Напиток готовится на конкретной физической кофе-машине, располагающейся в какой-то точке пространства.
4. Наши кофе-машины как-то распределены в пространстве (и времени).
5. Кофе-машина принадлежит какой-то сети кофеен, каждая из которых обладает какой-то айдентикой и специальными возможностями.
Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы прежде всего стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей: Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы прежде всего стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей:
@@ -22,28 +20,41 @@
Допустим, мы имеем следующий интерфейс: Допустим, мы имеем следующий интерфейс:
* `GET /v1/recipes/lungo` ```
возвращает рецепт лунго; // возвращает рецепт лунго
* `POST /v1/coffee-machines/orders?machine_id={id}` GET /v1/recipes/lungo
`{recipe:"lungo"}` ```
— размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа; ```
* `GET /v1/orders?order_id={id}` // размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа
— возвращает состояние заказа; POST /v1/coffee-machines/orders?machine_id={id}
{
"recipe": "lungo"
}
```
```
// возвращает состояние заказа
GET /v1/orders?order_id={id}
```
И зададимся вопросом, каким образом разработчик определит, что заказ клиента готов. Допустим, мы сделаем так: добавим в рецепт лунго эталонный объём, а в состояние заказа — количество уже налитого кофе. Тогда разработчику нужно будет проверить совпадение этих двух цифр, чтобы убедиться, что кофе готов. И зададимся вопросом, каким образом разработчик определит, что заказ клиента готов. Допустим, мы сделаем так: добавим в рецепт лунго эталонный объём, а в состояние заказа — количество уже налитого кофе. Тогда разработчику нужно будет проверить совпадение этих двух цифр, чтобы убедиться, что кофе готов.
Такое решение выглядит интуитивно плохим, и это действительно так: оно нарушает все вышеперечисленные принципы: Такое решение выглядит интуитивно плохим, и это действительно так: оно нарушает все вышеперечисленные принципы.
1. Для решения задачи «заказать лунго» разработчику нужно обратиться к сущности «рецепт» и выяснить, что у каждого рецепта есть объём. Далее, нужно принять концепцию, что приготовление кофе заканчивается в тот момент, когда объём сравнялся с эталонным. Нет никакого способа об этой конвенции догадаться: она неочевидна и её нужно найти в документации. При этом никакой пользы для разработчика в этом знании нет. 1. Для решения задачи «заказать лунго» разработчику нужно обратиться к сущности «рецепт» и выяснить, что у каждого рецепта есть объём. Далее, нужно принять концепцию, что приготовление кофе заканчивается в тот момент, когда объём сравнялся с эталонным. Нет никакого способа об этой конвенции догадаться: она неочевидна и её нужно найти в документации. При этом никакой пользы для разработчика в этом знании нет.
2. Мы автоматически получаем проблемы, если захотим варьировать размер кофе. Допустим, в какой-то момент мы захотим представить пользователю выбор, сколько конкретно миллилитров лунго он желает. Тогда нам придётся проделать один из следующих трюков: 2. Мы автоматически получаем проблемы, если захотим варьировать размер кофе. Допустим, в какой-то момент мы захотим представить пользователю выбор, сколько конкретно миллилитров лунго он желает. Тогда нам придётся проделать один из следующих трюков:
* или мы фиксируем список допустимых объёмов и заводим фиктивные рецепты типа `/recipes/small-lungo`, `recipes/large-lungo`. Почему фиктивные? Потому что рецепт один и тот же, меняется только объём. Нам придётся либо тиражировать одинаковые рецепты, отличающиеся только объёмом, либо вводить какое-то «наследование» рецептов, чтобы можно было указать базовый рецепт и только переопределить объём; * или мы фиксируем список допустимых объёмов и заводим фиктивные рецепты типа `/recipes/small-lungo`, `recipes/large-lungo`. Почему фиктивные? Потому что рецепт один и тот же, меняется только объём. Нам придётся либо тиражировать одинаковые рецепты, отличающиеся только объёмом, либо вводить какое-то «наследование» рецептов, чтобы можно было указать базовый рецепт и только переопределить объём;
* или мы модифицируем интерфейс, объявляя объём кофе, указанный в рецепте, значением по умолчанию; при размещении заказа мы разрешаем указать объём, отличный от эталонного: * или мы модифицируем интерфейс, объявляя объём кофе, указанный в рецепте, значением по умолчанию; при размещении заказа мы разрешаем указать объём, отличный от эталонного:
`POST /v1/coffee-machines/orders?machine_id={id}` ```
`{recipe:"lungo","volume":"800ml"}` POST /v1/coffee-machines/orders?machine_id={id}
Для таких кофе произвольного объёма нужно будет получать требуемый объём не из `GET /v1/recipes`, а из `GET /v1/orders`. Сделав так, мы сразу получаем клубок из связанных проблем: {
* разработчик, которому придётся поддержать эту функциональность, имеет высокие шансы сделать ошибку: добавив поддержку произвольного объёма кофе в код, работающий с `POST /v1/coffee-machines/orders` нужно не забыть переписать код проверки готовности заказа; "recipe":"lungo",
* мы получим классическую ситуацию, когда одно и то же поле (объём кофе) значит разные вещи в разных интерфейсах. В `GET /v1/recipes` поле «объём» теперь значит «объём, который будет запрошен, если не передать его явно в `POST /v1/coffee-machines/orders`»; переименовать его в «объём по умолчанию» уже не получится, с этой проблемой теперь придётся жить. "volume":"800ml"
}
```
Для таких кофе произвольного объёма нужно будет получать требуемый объём не из `GET /v1/recipes`, а из `GET /v1/orders`. Сделав так, мы сразу получаем клубок из связанных проблем:
* разработчик, которому придётся поддержать эту функциональность, имеет высокие шансы сделать ошибку: добавив поддержку произвольного объёма кофе в код, работающий с `POST /v1/coffee-machines/orders` нужно не забыть переписать код проверки готовности заказа;
* мы получим классическую ситуацию, когда одно и то же поле (объём кофе) значит разные вещи в разных интерфейсах. В `GET /v1/recipes` поле «объём» теперь значит «объём, который будет запрошен, если не передать его явно в `POST /v1/coffee-machines/orders`»; переименовать его в «объём по умолчанию» уже не получится, с этой проблемой теперь придётся жить.
3. Вся эта схема полностью неработоспособна, если разные модели кофе-машин производят лунго разного объёма. Для решения задачи «объём лунго зависит от вида машины» нам придётся сделать совсем неприятную вещь: сделать рецепт зависимым от id машины. Тем самым мы начнём активно смешивать уровни абстракции: одной частью нашего API (рецептов) станет невозможно пользоваться без другой части (информации о кофе-машинах). Что немаловажно, от разработчиков потребуется изменить логику своего приложения: если раньше они могли предлагать сначала выбрать объём, а потом кофе-машину, то теперь им придётся полностью изменить этот шаг. 3. Вся эта схема полностью неработоспособна, если разные модели кофе-машин производят лунго разного объёма. Для решения задачи «объём лунго зависит от вида машины» нам придётся сделать совсем неприятную вещь: сделать рецепт зависимым от id машины. Тем самым мы начнём активно смешивать уровни абстракции: одной частью нашего API (рецептов) станет невозможно пользоваться без другой части (информации о кофе-машинах). Что немаловажно, от разработчиков потребуется изменить логику своего приложения: если раньше они могли предлагать сначала выбрать объём, а потом кофе-машину, то теперь им придётся полностью изменить этот шаг.
@@ -93,8 +104,9 @@
* Машины с предустановленными программами: * Машины с предустановленными программами:
``` ```
GET /programs
// Возвращает список предустановленных программ // Возвращает список предустановленных программ
GET /programs
{ {
// идентификатор программы // идентификатор программы
"program": "01", "program": "01",
@@ -103,13 +115,14 @@
} }
``` ```
``` ```
// Запускает указанную программу на исполнение
// и возвращает статус исполнения
POST /execute POST /execute
{ {
"program": 1, "program": 1,
"volume": "200ml" "volume": "200ml"
} }
// Запускает указанную программу на исполнение
// и возвращает статус исполнения
{ {
// Уникальный идентификатор задания // Уникальный идентификатор задания
"execution_id": "01-01", "execution_id": "01-01",
@@ -120,21 +133,22 @@
} }
``` ```
``` ```
POST /cancel
// Отменяет текущую программу // Отменяет текущую программу
POST /cancel
``` ```
``` ```
GET /execution/status
// Возвращает статус исполнения // Возвращает статус исполнения
// Формат аналогичен формату ответа `POST /execute` // Формат аналогичен формату ответа `POST /execute`
GET /execution/status
``` ```
**NB**. На всякий случай отметим, что данное API нарушает множество описанных нами принципов проектирования, начиная с отсутствия версионирования; оно приведено в таком виде по двум причинам: (1) чтобы мы могли показать, как спроектировать API более удачно; (2) скорее всего, в реальной жизни вы получите именно такое API от производителей кофе-машин, и это ещё довольно вменяемый вариант. **NB**. На всякий случай отметим, что данное API нарушает множество описанных нами принципов проектирования, начиная с отсутствия версионирования; оно приведено в таком виде по двум причинам: (1) чтобы мы могли показать, как спроектировать API более удачно; (2) скорее всего, в реальной жизни вы получите именно такое API от производителей кофе-машин, и это ещё довольно вменяемый вариант.
* Машины с предустановленными функциями: * Машины с предустановленными функциями:
``` ```
GET /functions
// Возвращает список доступных функций // Возвращает список доступных функций
GET /functions
{ {
"functions": [ "functions": [
{ {
@@ -154,17 +168,18 @@
} }
``` ```
``` ```
// Запускает на исполнение функцию
// с передачей указанных значений аргументов
POST /functions POST /functions
{ {
"type": "set_cup", "type": "set_cup",
"arguments": [{ "name": "volume", "value": "300ml" }] "arguments": [{ "name": "volume", "value": "300ml" }]
} }
// Запускает на исполнение функцию
// с передачей указанных значений аргументов
``` ```
``` ```
GET /sensors
// Возвращает статусы датчиков // Возвращает статусы датчиков
GET /sensors
{ {
"sensors": [ "sensors": [
{ {
@@ -208,15 +223,19 @@
Что это будет означать практически? Разработчик по-прежнему будет создавать заказ, оперируя только высокоуровневыми терминами: Что это будет означать практически? Разработчик по-прежнему будет создавать заказ, оперируя только высокоуровневыми терминами:
``` ```
POST /v1/coffee-machines/orders?machine_id={id} POST /v1/coffee-machines/orders?machine_id={id}
{recipe:"lungo","volume":"800ml"} {
"recipe": "lungo",
"volume": "800ml"
}
{ "order_id" }
``` ```
Имплементация функции `POST /orders` проверит все параметры заказа, заблокирует его стоимость на карте пользователя, сформирует полный запрос на исполнение и обратится к уровню исполнения. Сначала необходимо подобрать правильную программу исполнения: Имплементация функции `POST /orders` проверит все параметры заказа, заблокирует его стоимость на карте пользователя, сформирует полный запрос на исполнение и обратится к уровню исполнения. Сначала необходимо подобрать правильную программу исполнения:
``` ```
POST /v1/programs/match POST /v1/programs/match
{ "recipe", "coffee-machine" } { "recipe", "coffee-machine" }
```
```
{ "program_id" } { "program_id" }
``` ```
Получив идентификатор программы, нужно запустить её на исполнение: Получив идентификатор программы, нужно запустить её на исполнение:
@@ -232,8 +251,7 @@ POST /v1/programs/{id}/run
} }
] ]
} }
```
```
{ "program_run_id" } { "program_run_id" }
``` ```
@@ -250,8 +268,7 @@ POST /v1/programs/{id}/run
``` ```
POST /v1/runtimes POST /v1/runtimes
{ "coffee_machine", "program", "parameters" } { "coffee_machine", "program", "parameters" }
```
```
{ "runtime_id", "state" } { "runtime_id", "state" }
``` ```
Здесь `program` будет выглядеть примерно так: Здесь `program` будет выглядеть примерно так:

View File

@@ -20,28 +20,28 @@
* **Плохо**: * **Плохо**:
``` ```
// Отменяет заказ
GET /orders/cancellation GET /orders/cancellation
// отменяет заказ
``` ```
Неочевидно, что достаточно просто обращения к сущности `cancellation` (что это?), тем более немодифицирующим методом `GET`, чтобы отменить заказ; Неочевидно, что достаточно просто обращения к сущности `cancellation` (что это?), тем более немодифицирующим методом `GET`, чтобы отменить заказ;
**Хорошо**: **Хорошо**:
``` ```
// Отменяет заказ
POST /orders/cancel POST /orders/cancel
отменяет заказ
``` ```
* **Плохо**: * **Плохо**:
``` ```
GET /orders/statistics
// Возвращает агрегированную статистику заказов за всё время // Возвращает агрегированную статистику заказов за всё время
GET /orders/statistics
``` ```
Даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы вызов операции без параметров максимально расходовал ресурсы. Даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы вызов операции без параметров максимально расходовал ресурсы.
**Хорошо**: **Хорошо**:
``` ```
// Возвращает агрегированную статистику заказов за указанный период
POST /orders/statistics/aggregate POST /orders/statistics/aggregate
{ "start_date", "end_date" } { "start_date", "end_date" }
// Возвращает агрегированную статистику заказов за указанный период
``` ```
**Стремитесь к тому, чтобы из сигнатуры функции было абсолютно ясно, что она делает, что принимает на вход и что возвращает**. Вообще, при прочтении кода, работающего с вашим API, должно быть сразу понятно, что, собственно, он делает — без подглядывания в документацию. **Стремитесь к тому, чтобы из сигнатуры функции было абсолютно ясно, что она делает, что принимает на вход и что возвращает**. Вообще, при прочтении кода, работающего с вашим API, должно быть сразу понятно, что, собственно, он делает — без подглядывания в документацию.
@@ -92,9 +92,9 @@
**Хорошо**: `order.get_estimated_delivery_time()` **Хорошо**: `order.get_estimated_delivery_time()`
* **Плохо**: * **Плохо**:
``` ```
strpbrk (str1, str2)
// возвращает положение первого вхождения в строку str2 // возвращает положение первого вхождения в строку str2
// любого символа из строки str2 // любого символа из строки str2
strpbrk (str1, str2)
``` ```
Возможно, автору этого API казалось, что аббревиатура `pbrk` что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк `str1`, `str2` является набором символов для поиска. Возможно, автору этого API казалось, что аббревиатура `pbrk` что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк `str1`, `str2` является набором символов для поиска.
**Хорошо**: `str_search_for_characters(lookup_character_set, str)` **Хорошо**: `str_search_for_characters(lookup_character_set, str)`
@@ -118,8 +118,8 @@
* **Плохо**: * **Плохо**:
``` ```
GET /coffee-machines/functions
// Возвращает список встроенных функций кофе-машины // Возвращает список встроенных функций кофе-машины
GET /coffee-machines/functions
``` ```
Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует). Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).
**Хорошо**: `GET /coffee-machines/builtin-functions-list` **Хорошо**: `GET /coffee-machines/builtin-functions-list`
@@ -131,14 +131,14 @@
**Хорошо**: `begin_transition` / `end_transition` либо `start_transition` / `stop_transition`. **Хорошо**: `begin_transition` / `end_transition` либо `start_transition` / `stop_transition`.
* **Плохо**: * **Плохо**:
``` ```
strpos(haystack, needle)
// Находит первую позицию позицию строки `needle` // Находит первую позицию позицию строки `needle`
// внутри строки `haystack` // внутри строки `haystack`
strpos(haystack, needle)
``` ```
``` ```
str_replace(needle, replace, haystack)
// Находит и заменяет все вхождения строки `needle` // Находит и заменяет все вхождения строки `needle`
// внутри строки `haystack` на строку `replace` // внутри строки `haystack` на строку `replace`
str_replace(needle, replace, haystack)
``` ```
Здесь нарушены сразу несколько правил: Здесь нарушены сразу несколько правил:
* написание неконсистентно в части знака подчеркивания; * написание неконсистентно в части знака подчеркивания;
@@ -153,18 +153,16 @@
* **Плохо**: * **Плохо**:
``` ```
// Создаёт комментарий и возвращает его id
POST /comments POST /comments
{ "content" } { "content" }
// Создаёт комментарий и возвращает его id
```
```
{ "comment_id" } { "comment_id" }
``` ```
``` ```
GET /comments/{id}
// Возвращает комментарий по его id // Возвращает комментарий по его id
``` GET /comments/{id}
```
{ {
// Комментарий не опубликован // Комментарий не опубликован
// и ждёт прохождения капчи // и ждёт прохождения капчи
@@ -176,20 +174,19 @@
— хотя операция будто бы выполнена успешна, клиенту необходимо сделать дополнительный запрос, чтобы понять необходимость решения капчи. Между вызовами `POST /comments` и `GET /comments/{id}` клиент находится в состоянии кота Шрёдингера: непонятно, опубликован комментарий или нет, и как отразить это пользователю. — хотя операция будто бы выполнена успешна, клиенту необходимо сделать дополнительный запрос, чтобы понять необходимость решения капчи. Между вызовами `POST /comments` и `GET /comments/{id}` клиент находится в состоянии кота Шрёдингера: непонятно, опубликован комментарий или нет, и как отразить это пользователю.
**Хорошо**: **Хорошо**:
``` ```
// Создаёт комментарий и возвращает его
POST /comments POST /comments
{ "content" } { "content" }
// Создаёт комментарий и возвращает его
```
```
{ "comment_id", "published", "action_required", "content" } { "comment_id", "published", "action_required", "content" }
``` ```
``` ```
GET /comments/{id}
// Возвращает комментарий по его id // Возвращает комментарий по его id
``` GET /comments/{id}
```
{ /* в точности тот же формат, { /* в точности тот же формат,
что и в ответе POST /comments */ что и в ответе POST /comments */
} }
``` ```
Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа невелик) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение. Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа невелик) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.