1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-01-17 17:44:13 +02:00

Настроена сборка

This commit is contained in:
Sergey Konstantinov 2020-11-05 12:14:16 +03:00
parent 0b53603f2c
commit af1159c08b
10 changed files with 498 additions and 51 deletions

5
.gitignore vendored
View File

@ -1 +1,4 @@
.vscode
.vscode
node_modules
package-lock.json
*/desktop.ini

9
build_html.js Normal file
View File

@ -0,0 +1,9 @@
const fs = require('fs');
const mdHtml = new (require('showdown').Converter)();
const md = fs.readFileSync('./src/API.ru.md', 'utf-8');
const html = `<html><head><meta charset="utf-8"/><title>Сергей Константинов. API</title></head>
<body><article>${mdHtml.makeHtml(md)}</article></body>
</html>`;
process.stdout.write(html);

2
build_pdf.js Normal file
View File

@ -0,0 +1,2 @@
const fs = require('fs');
process.stdout.write(fs.readFileSync('./dist/API.ru.html', 'utf-8'));

405
dist/API.ru.html vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/API.ru.pdf vendored Normal file

Binary file not shown.

16
package.json Normal file
View File

@ -0,0 +1,16 @@
{
"description": "A book on APIs",
"homepage": "https://github.com/twirl/The-API-Book",
"license": "CC-BY-NC-4.0",
"author": "Sergey Konstantinov <twirl-team@yandex.ru>",
"repository": "github.com:twirl/The-API-Book",
"devDependencies": {
"html-pdf": "^2.2.0",
"percollate": "^1.0.1",
"showdown": "^1.9.1"
},
"scripts": {
"build-html": "node build_html.js | percollate html -o ./dist/API.ru.html --style=./src/style.css -",
"build-pdf": "node build_pdf.js | percollate pdf -o ./dist/API.ru.pdf --style=./src/style.css -"
}
}

View File

@ -76,7 +76,7 @@
### О версионировании
Здесь и далее мы будем придерживаться принципов версионирования ((https://semver.org/ semver)):
Здесь и далее мы будем придерживаться принципов версионирования [semver](https://semver.org/):
1. Версия API задаётся тремя цифрами, вида `1.2.3`
2. Первая цифра (мажорная версия) увеличивается при обратно несовместимых изменениях в API
@ -183,9 +183,13 @@ _NB_. Здесь и далее мы будем рассматривать кон
Допустим, мы имеем следующий интерфейс:
* `GET /recipes/lungo` возвращает рецепт лунго;
* `POST /coffee-machines/orders?machine_id={id}` `{recipe:"lungo"}` размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа;
* `GET /orders?order_id={id}` возвращает состояние заказа;
* `GET /recipes/lungo`
— возвращает рецепт лунго;
* `POST /coffee-machines/orders?machine_id={id}`
`{recipe:"lungo"}`
— размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа;
* `GET /orders?order_id={id}`
— возвращает состояние заказа;
И зададимся вопросом, каким образом разработчик определит, что заказ клиента готов. Допустим, мы сделаем так: добавим в рецепт лунго эталонный объём, а в состояние заказа — количество уже налитого кофе. Тогда разработчику нужно будет проверить совпадение этих двух цифр, чтобы убедиться, что кофе готов.
@ -195,8 +199,9 @@ _NB_. Здесь и далее мы будем рассматривать кон
2. Мы автоматически получаем проблемы, если захотим варьировать размер кофе. Допустим, в какой-то момент мы захотим представить пользователю выбор, сколько конкретно миллилитров лунго он желает. Тогда нам придётся проделать один из следующих трюков:
* или мы фиксируем список допустимых объёмов и заводим фиктивные рецепты типа `/recipes/small-lungo`, `recipes/large-lungo`. Почему фиктивные? Потому что рецепт один и тот же, меняется только объём. Нам придётся либо тиражировать одинаковые рецепты, отличающиеся только объёмом, либо вводить какое-то «наследование» рецептов, чтобы можно было указать базовый рецепт и только переопределить объём;
* или мы модифицируем интерфейс, объявляя объём кофе, указанный в рецепте, значением по умолчанию; при размещении заказа мы разрешаем указать объём, отличный от эталонного:
`POST /coffee-machines/orders?machine_id={id}` `{recipe:"lungo","volume":"800ml"}`
* или мы модифицируем интерфейс, объявляя объём кофе, указанный в рецепте, значением по умолчанию; при размещении заказа мы разрешаем указать объём, отличный от эталонного:
`POST /coffee-machines/orders?machine_id={id}`
`{recipe:"lungo","volume":"800ml"}`
Для таких кофе произвольного объёма нужно будет получать требуемый объём не из `GET /recipes`, а из `GET /orders`. Сделав так, мы сразу получаем клубок из связанных проблем:
* разработчик, которому придётся поддержать эту функциональность, имеет высокие шансы сделать ошибку: добавив поддержку произвольного объёма кофе в код, работающий с `POST /coffee-machines/orders` нужно не забыть переписать код проверки готовности заказа;
* мы получим классическую ситуацию, когда одно и то же поле (объём кофе) значит разные вещи в разных интерфейсах. В `GET /recipes` поле «объём» теперь значит «объём, который будет запрошен, если не передать его явно в `POST /coffee-machines/orders`; переименовать его в «объём по умолчанию» уже не получиться, с этой проблемой теперь придётся жить.
@ -205,13 +210,13 @@ _NB_. Здесь и далее мы будем рассматривать кон
Хорошо, допустим, мы поняли, как сделать плохо. Но как же тогда сделать *хорошо*? Разделение уровней абстракции должно происходить вдоль трёх направлений:
1. От сценариев использования к их внутренней реализации: высокоуровневые сущности и номенклатура их методов должны напрямую отражать сценарии использования API; низкоуровневый - отражать декомпозицию сценариев на составные части.
1. От сценариев использования к их внутренней реализации: высокоуровневые сущности и номенклатура их методов должны напрямую отражать сценарии использования API; низкоуровневый - отражать декомпозицию сценариев на составные части.
Здесь мы должны явно обратиться к выписанному нами ранее «что» и «как». В идеальном мире высший уровень абстракции вашего API должен быть просто переводом записанной человекочитаемой фразы на машинный язык. Если нужно узнать, готов ли заказ — значит, должен быть метод `is-order-ready` (если мы считаем эту операцию действительно важной и частотной) или хотя бы `GET /orders/{id}/status` для того, чтобы явно узнать статус заказа. Эту логику требуется прорастить вниз до самых мелких и частных сценариев типа определения температуры напитка или наличия у исполнителя картонного держателя нужного размера.
Здесь мы должны явно обратиться к выписанному нами ранее «что» и «как». В идеальном мире высший уровень абстракции вашего API должен быть просто переводом записанной человекочитаемой фразы на машинный язык. Если нужно узнать, готов ли заказ — значит, должен быть метод `is-order-ready` (если мы считаем эту операцию действительно важной и частотной) или хотя бы `GET /orders/{id}/status` для того, чтобы явно узнать статус заказа. Эту логику требуется прорастить вниз до самых мелких и частных сценариев типа определения температуры напитка или наличия у исполнителя картонного держателя нужного размера.
2. От терминов предметной области пользователя к терминам предметной области исходных данных — в нашем случае от высокоуровневых понятий «рецепт», «заказ», «бренд», «кофейня» к низкоуровневым «температура напитка» и «координаты кофе-машины»
2. От терминов предметной области пользователя к терминам предметной области исходных данных — в нашем случае от высокоуровневых понятий «рецепт», «заказ», «бренд», «кофейня» к низкоуровневым «температура напитка» и «координаты кофе-машины»
3. Наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к «сырым» - в нашем случае от «лунго» и «сети кофеен "Ромашка"» - к сырым байтовый данным, описывающим состояние кофе-машины марки «Доброе утро» в процессе приготовления напитка.
3. Наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к «сырым» - в нашем случае от «лунго» и «сети кофеен "Ромашка"» - к сырым байтовый данным, описывающим состояние кофе-машины марки «Доброе утро» в процессе приготовления напитка.
Чем дальше находятся друг от друга программные контексты, которые соединяет наше API - тем более глубокая иерархия сущностей должна получиться у нас в итоге.
@ -231,22 +236,22 @@ _NB_. Здесь и далее мы будем рассматривать кон
Внимательный читатель может здесь поинтересоваться, а в чём, собственно разница по сравнению с наивным подходом? Напомню, мы рассмотрели выше примерно такой вариант:
* `POST /coffee-machines/orders?machine_id={id}`\
`{recipe:"lungo","volume":"800ml"}`\
* `POST /coffee-machines/orders?machine_id={id}`
`{recipe:"lungo","volume":"800ml"}`
— создаёт заказ указанного объёма
* `GET /orders/{id}`\
`{…"volume_requested":"800ml","volume_prepared":"120ml"…}`\
* `GET /orders/{id}`
`{…"volume_requested":"800ml","volume_prepared":"120ml"…}`
— состояние исполнения заказа (налито 120 мл из запрошенных 800).
По сути пара `volume_requested`/`volume_prepared` и является аналогом дополнительной сущности `task`, зачем мы тогда усложняли?
По сути пара `volume_requested` / `volume_prepared` и является аналогом дополнительной сущности `task`, зачем мы тогда усложняли?
Во-первых, в схеме с дополнительным уровнем абстракции мы скрываем конструирование самого объекта `task`. Если от `GET /orders/{id}` ожидается, что он вернёт хотя бы логически те же параметры заказа, что были переданы в `POST /coffee-machines/orders`, то при конструировании `task` сформировать нужный набор параметров — уже наша ответственность, спрятанная внутри обработчика создания заказа. Мы можем переформулировать параметры заказа в более удобные для исполнения на кофе-машине термины — например, возвращаясь к вопросу проверки готовности, явно сформулировать политику определения готовности кофе:
* `POST /tasks/?order_id={order_id}`\
`{…"volume_requested":"800ml","readiness_policy":"check_volume"…}`\
* `POST /tasks/?order_id={order_id}`
`{…"volume_requested":"800ml","readiness_policy":"check_volume"…}`
— внутри обработчика создания заказа мы обратились к спецификации кофе-машины и поставили задачу в соответствии с ней. (Здесь мы предполагаем, что `POST /tasks` — внутренний метод создания задач; он может и не существовать в виде API.)
* `GET /tasks/{id}/status`\
`{…"volume_prepared":"200ml","ready":false}`
* `GET /tasks/{id}/status`
`{…"volume_prepared":"200ml","ready":false}`
— в публичном интерфейсе
На это (совершенно верное!) замечаниемы ответим, что выделение уровней абстракции — прежде всего _логическая_ процедура: как мы объясняем себе и разработчику, из чего состоит наш API. Мы могли бы просто ограничиться выделением секции `task` в ответе `GET /orders/{id}` — или вовсе сказать, что `task` — это просто четверка полей (`ready`, `volume_requested`, `volume_prepared`, `readiness_policy`) и есть. **Абстрагируемая дистанция между сущностями существует объективно**, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни _явно_. Чем неявнее разведены уровни абстракции (или хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API и тем хуже будет написан использующий его код.
@ -280,11 +285,11 @@ NB: важно заметить, что с дальнейшей проработ
Обратите также внимание, что содержание операции «отменить заказ» изменяется на каждом из уровней. На пользовательском уровне заказ отменён, когда решены все важные для пользователя вопросы. То, что отменённый заказ какое-то время продолжает исполняться (например, ждёт утилизации) — пользователю неважно. На уровне исполнения же нужно связать оба контекста:
* `GET /tasks/{id}/status`\
`{"status":"canceled","finished":false,"operations":[…]}`\
С т.з. высокоуровневого кода задача завершена (`canceled`), но с точки зрения низкоуровневого кода список исполняемых операций непуст, т.е. задача продолжает работать.
* `GET /tasks/{id}/status`
`{"status":"canceled","operations":{"status":"canceling","items":[…]}`
— с т.з. высокоуровневого кода задача завершена (`canceled`), но с точки зрения низкоуровневого кода список исполняемых операций непуст, т.е. задача продолжает работать.
NB: так как `task` связывает два разных уровня абстракции, то и статусов у неё два: внешний `canceled` и внутренний `finished`. Мы могли бы опустить второй статус и предложить ориентироваться на содержание `operations`, но это вновь (а) неявно, (б) предполагает необходимость разбираться в более низкоуровневом интерфейсе `operation`, что, быть может, разработчику вовсе и не нужно.
NB: так как `task` связывает два разных уровня абстракции, то и статусов у неё два: внешний `canceled` и внутренний `canceling`. Мы могли бы опустить второй статус и предложить ориентироваться на содержание `operations`, но это вновь (а) неявно, (б) предполагает необходимость разбираться в более низкоуровневом интерфейсе `operations`, что, быть может, разработчику вовсе и не нужно.
Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы оно выполняло свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой.

View File

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

34
src/style.css Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,27 +0,0 @@
body {
width: 60%;
max-width: 1000px;
margin: 10px auto 0 300px;
font-family: Georgia, serif;
font-size: 18px;
}
header {
text-align: center;
background: url(Why.jpg) center 0 no-repeat;
background-size: auto;
height: 600px;
position: relative;
}
header h1 {
color: #fff;
margin: 0 20%;
}
h1, h2, h3, h4 {
font-weight: bold;
margin: 0;
padding: 0.2em 0 0.2em 0;
text-align: center;
}