mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-01-17 17:44:13 +02:00
Настроена сборка
This commit is contained in:
parent
0b53603f2c
commit
af1159c08b
5
.gitignore
vendored
5
.gitignore
vendored
@ -1 +1,4 @@
|
||||
.vscode
|
||||
.vscode
|
||||
node_modules
|
||||
package-lock.json
|
||||
*/desktop.ini
|
9
build_html.js
Normal file
9
build_html.js
Normal 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
2
build_pdf.js
Normal 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
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
BIN
dist/API.ru.pdf
vendored
Normal file
Binary file not shown.
16
package.json
Normal file
16
package.json
Normal 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 -"
|
||||
}
|
||||
}
|
@ -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 будет нулевой.
|
||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
34
src/style.css
Normal file
34
src/style.css
Normal file
File diff suppressed because one or more lines are too long
27
style.css
27
style.css
@ -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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user