diff --git a/docs/API.ru.html b/docs/API.ru.html index b9c56f8..ba50292 100644 --- a/docs/API.ru.html +++ b/docs/API.ru.html @@ -136,7 +136,32 @@ h4, h5 {
Разработка программного обеспечения характеризуется, помимо прочего, существованием множества различных парадигм разработки, адепты которых зачастую настроены весьма воинственно по отношению к адептам других парадигм. Поэтому при написании этой книги мы намеренно избегаем слов «метод», «объект», «функция» и так далее, используя нейтральный термин «сущность». Под «сущностью» понимается некоторая атомарная единица функциональности — класс, метод, объект, монада, прототип (нужное подчеркнуть).
Для составных частей сущности, к сожалению, достаточно нейтрального термина нам придумать не удалось, поэтому мы используем слова «поля» и «методы».
Большинство примеров API в общих разделах будут даны в виде JSON-over-HTTP-эндпойтов. Это некоторая условность, которая помогает описать концепции, как нам кажется, максимально понятно. Вместо GET /v1/orders
вполне может быть вызов метода orders.get()
, локальный или удалённый; вместо JSON может быть любой другой формат данных. Смысл утверждений от этого не меняется.
Также в примерах часто применяется следующая конвенция. Запись { "begin_date" }
(т.е. отсутствие значения у поля в JSON-объекте) означает, что в поле находится именно то, что ожидается — т.е. в данном примере какая-то дата начала.
Рассмотрим следующую запись:
+POST /v1/bucket/{id}/some-resource
+{
+ …
+ // Это однострочный комментарий
+ "some_parameter": "value",
+ …
+}
+
+{
+ /* А это многострочный
+ комментарий */
+ "operation_id"
+}
+
+Её следует читать так:
+/v1/bucket/{id}/some-resource
, где {id}
заменяется на некоторый идентификатор bucket
-а (при отсутствии уточнений подстановки вида {something}
следует относить к ближайшему термину слева);some_parameter
со значением value
и ещё какие-то поля, которые для краткости опущены (что показано многоточием);operation_id
; отсутствие значения поля означает, что его значением является именно то, что в этом поле и ожидается — в данном случае какой-то идентификатор операции.Для упрощения неважен возможна сокращенная запись вида:
+POST /v1/bucket/{id}/some-resource
{…,"some_parameter",…}
— если тела ответа нет или оно нам не понадобится в ходе рассмотрения примера.Чтобы сослаться на это описание будут использоваться выражения типа «метод POST /v1/bucket/{id}/some-resource
» или, для простоты, «метод /some-resource
» (если никаких других some-resource
в контексте главы не упоминается и перепутать не с чем).
Подход, который мы используем для проектирования, состоит из четырёх шагов:
POST /cancel
// Отменяет текущую программу
-GET /execution/{execution_id}/status
+GET /execution/status
// Возвращает статус исполнения
// Формат аналогичен формату ответа `POST /execute`
@@ -354,18 +375,69 @@ h4, h5 {
Очевидно, что разработчику хочется создавать заказ унифицированным образом — перечислить высокоуровневые параметры заказа (вид напитка, объём и специальные требования, такие как вид сиропа или молока) — и не думать о том, как на конкретной машине исполнить этот заказ.
Разработчику надо понимать состояние исполнения — готов ли заказ или нет; если не готов — когда ожидать готовность (и надо ли её ожидать вообще в случае ошибки исполнения).
Разработчику нужно уметь соотносить заказ с его положением в пространстве и времени — чтобы показать потребителю, когда и как нужно заказ забрать.
-Наконец, разработчику нужно выполнять атомарные операции — прежде всего, отменять заказ.
+Наконец, разработчику нужно выполнять атомарные операции — например, отменять заказ.
-Таким образом, наш промежуточный уровень абстракции должен:
+Заметим, что API первого типа гораздо ближе к потребностям разработчика, нежели API второго типа. Концепция атомарной «программы» гораздо ближе к удобному для разработчика интерфейсу, нежели работа с сырыми наборами команд и данными сенсоров. В API первого типа мы видим только две проблемы:
-- скрывать параметры создания задания на приготовление напитка — их вычисление должно производиться в нашем коде;
-- обобщать состояние исполнения в единый набор статусов, не зависящий от физического уровня приготовления напитка;
-- связывать параметры заказа (его уникальный идентификатор) с конкретным процессом приготовления напитка;
-- предоставлять атомарный интерфейс логических операций, скрывая за фасадом различия в физической имплементации этих операций.
+- отсутствие явного соответствия программ и рецептов; идентификатор программы по-хорошему вообще не нужен при работе с заказами, раз уже есть понятие рецепта;
+- отсутствие явного статуса готовности.
-Первая из проблем, которую мы видим на данном этапе — это отсутствие на физическом уровне машин второго типа самого понятия «заказ» или «напиток»: машина выполняет какой-то набор операций
-Выделение уровней абстракции — прежде всего логическая процедура: как мы объясняем себе и разработчику, из чего состоит наш API. Мы могли бы просто ограничиться выделением секции task
в ответе GET /orders/{id}
— или вовсе сказать, что task
— это просто четверка полей (ready
, volume_requested
, volume_prepared
, readiness_policy
) и есть. Абстрагируемая дистанция между сущностями существует объективно, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни явно. Чем неявнее разведены уровни абстракции (или хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API и тем хуже будет написан использующий его код.
-NB: важно заметить, что с дальнейшей проработкой уровень исполнения, скорее всего, сам должен будет разделиться на два и более уровня, т.к. «задача» по сути — просто сущность-зонтик, связывающая в рамках заказа несколько высокоуровневых сущностей. Идея определения параметров кофе-машины на этапе создания заказов не очень удобна, да и до манипуляции командами кофе-машины и состоянием сенсоров всё ещё далеко с точки зрения абстрагирования. Но мы пока оставим в таком виде, для удобства дальнейшего изложения.
+С API второго типа всё гораздо хуже. Главная проблема, которая нас ожидает — отсутствие «памяти» исполняемых действий. API функций и сенсоров полностью stateless; это означает, что мы даже не знаем, кем, когда и в рамках какого заказа была запущена текущая функция.
+Таким образом, нам нужно внедрить два новых уровня абстракции:
+
+Уровень управления исполнением, предоставляющий унифицированный интерфейс к атомарным программам. «Унифицированный интерфейс» в данном случае означает, что, независимо от того, на какого рода кофе-машине готовится заказ, разработчик может рассчитывать на:
+
+- единую номенклатуру статусов и других высокоуровневых параметров исполнения (например, ожидаемого времени готовности заказа или возможных ошибок исполнения);
+- единую номенклатуру доступных методов (например, отмены заказа) и их одинаковое поведение.
+Уровень программы исполнения. Для API первого типа он будет представлять собой просто обёртку над существующим API программ; для API второго типа концепцию «программ» придётся полностью имплементировать нам.
+
+Что это будет означать практически? Разработчик по-прежнему будет создавать заказ, оперируя только высокоуровневыми терминами:
+POST /v1/coffee-machines/orders?machine_id={id}
+{recipe:"lungo","volume":"800ml"}
+
+Имплементация функции POST /orders
проверит все параметры заказа, заблокирует его стоимость на карте пользователя, сформирует полный запрос на исполнение и обратится к уровню исполнения
+POST /v1/execute
+{
+ "order_id",
+ "coffee_machine",
+ "recipe",
+ "volume_requested": "800ml"
+}
+
+{
+ "execution_id": <идентификатор исполнения>
+}
+
+Далее нам нужно подобрать нужную программу исполнения:
+POST /v1/programs/match
+{ "recipe", "coffee-machine" }
+
+{ "program_id" }
+
+Наконец, обладая идентификатором нужной программы, мы можем её запустить:
+POST /v1/programs/{id}/run
+{
+ "execution_id",
+ "coffee_machine_id"
+}
+
+{ "program_run_id" }
+
+Обратите внимание, что во всей этой цепочке вообще никак не участвует тип API кофе-машины — собственно, ровно для этого мы и абстрагировали. Мы могли бы сделать интерфейсы более конкретными, разделив функциональность run
и match
для разных API, т.е. ввести раздельные endpoint-ы:
+
+POST /v1/programs/{api_type}/match
+POST /v1/programs/{api_type}/{program_id}/run
+
+Достоинством такого подхода была бы возможность передавать в match и run не унифицированные наборы параметров, а только те, которые имеют значение в контексте указанного типа API. Однако в нашем дизайне API такой необходимости не прослеживается.
+NB: в имплементации связки execute
→ match
→ run
можно пойти одним из двух путей:
+
+- либо
POST /orders
сама обращается к доступной информации о рецепте и кофе-машине и формирует stateless-запрос, в котором указаны все нужные данные (тип API кофе-машины в частности);
+- либо в запросе содержатся только идентификаторы, и имплементация методов сами обратятся за нужными данными через какие-то внутренние API.
+Оба варианта имеют право на жизнь; какой из них выбрать — зависит от деталей реализации.
+
+Любопытно, что введённая сущность match
связывает два уровня абстракции, и тем самым не относится ни к одному из них. Такая ситуация (когда некоторые вспомогательные сущности находятся вне общей иерархии) случается довольно часто.
+// TODO
+Выделение уровней абстракции — прежде всего логическая процедура: как мы объясняем себе и разработчику, из чего состоит наш API. Абстрагируемая дистанция между сущностями существует объективно, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни явно. Чем неявнее разведены уровни абстракции (или хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API и тем хуже будет написан использующий его код.
Изоляция уровней абстракции
Важное свойство правильно подобранных уровней абстракции, и отсюда требование к их проектированию — это требование изоляции: взамодействие возможно только между сущностями соседних уровней абстракции. Если при проектировании выясняется, что для выполнения того или иного действия требуется «перепрыгнуть» уровень абстракции, это явный признак того, что в проекте допущены ошибки.
Возвращаясь к нашему примеру с готовностью кофе: проблемы с определением готовности кофе исходя из объёма возникают именно потому, что мы не можем ожидать от пользователя, создающего заказ, знания о необходимости проверки объёма налитого реальной кофе-машиной объёма кофе. Мы вводим дополнительный уровень абстракции именно для того, чтобы на нём переформулировать, что такое «заказ готов».
diff --git a/docs/API.ru.pdf b/docs/API.ru.pdf
index 59dbcc4..5486b18 100644
Binary files a/docs/API.ru.pdf and b/docs/API.ru.pdf differ
diff --git a/src/ru/clean-copy/01-Введение/06.md b/src/ru/clean-copy/01-Введение/06.md
index f589fd3..392fbbc 100644
--- a/src/ru/clean-copy/01-Введение/06.md
+++ b/src/ru/clean-copy/01-Введение/06.md
@@ -6,4 +6,30 @@
Большинство примеров API в общих разделах будут даны в виде JSON-over-HTTP-эндпойтов. Это некоторая условность, которая помогает описать концепции, как нам кажется, максимально понятно. Вместо `GET /v1/orders` вполне может быть вызов метода `orders.get()`, локальный или удалённый; вместо JSON может быть любой другой формат данных. Смысл утверждений от этого не меняется.
-Также в примерах часто применяется следующая конвенция. Запись `{ "begin_date" }` (т.е. отсутствие значения у поля в JSON-объекте) означает, что в поле находится именно то, что ожидается — т.е. в данном примере какая-то дата начала.
\ No newline at end of file
+Рассмотрим следующую запись:
+```
+POST /v1/bucket/{id}/some-resource
+{
+ …
+ // Это однострочный комментарий
+ "some_parameter": "value",
+ …
+}
+```
+```
+{
+ /* А это многострочный
+ комментарий */
+ "operation_id"
+}
+```
+
+Её следует читать так:
+ * выполняется POST-запрос к ресурсу `/v1/bucket/{id}/some-resource`, где `{id}` заменяется на некоторый идентификатор `bucket`-а (при отсутствии уточнений подстановки вида `{something}` следует относить к ближайшему термину слева);
+ * в качестве тела запроса передаётся JSON, содержащий поле `some_parameter` со значением `value` и ещё какие-то поля, которые для краткости опущены (что показано многоточием);
+ * телом ответа является JSON, состоящий из единственного поля `operation_id`; отсутствие значения поля означает, что его значением является именно то, что в этом поле и ожидается — в данном случае какой-то идентификатор операции.
+
+Для упрощения неважен возможна сокращенная запись вида:
+ * `POST /v1/bucket/{id}/some-resource` `{…,"some_parameter",…}` — если тела ответа нет или оно нам не понадобится в ходе рассмотрения примера.
+
+Чтобы сослаться на это описание будут использоваться выражения типа «метод `POST /v1/bucket/{id}/some-resource`» или, для простоты, «метод `/some-resource`» (если никаких других `some-resource` в контексте главы не упоминается и перепутать не с чем).
\ No newline at end of file
diff --git a/src/ru/clean-copy/02-I. Проектирование API/03.md b/src/ru/clean-copy/02-I. Проектирование API/03.md
index 0e3a697..386c5a4 100644
--- a/src/ru/clean-copy/02-I. Проектирование API/03.md
+++ b/src/ru/clean-copy/02-I. Проектирование API/03.md
@@ -116,11 +116,7 @@
// Идентификатор исполняемой программы
"program": 1,
// Запрошенный объём напитка
- "volume": "200ml",
- // Ожидаемое время приготовления
- "preparation_time": "20s",
- // Готовность
- "ready": false
+ "volume": "200ml"
}
```
```
@@ -128,7 +124,7 @@
// Отменяет текущую программу
```
```
- GET /execution/{execution_id}/status
+ GET /execution/status
// Возвращает статус исполнения
// Формат аналогичен формату ответа `POST /execute`
```
@@ -183,28 +179,89 @@
]
}
```
-
+
_NB_. Пример нарочно сделан умозрительным для моделирования ситуации, описанной в начале главы: для определения готовности напитка нужно сличить объём налитого с эталоном.
Теперь картина становится более явной: нам нужно абстрагировать работу с кофе-машиной так, чтобы наш «уровень исполнения» в API предоставлял общие функции (такие, как определение готовности напитка) в унифицированном виде. Важно отметить, что с точки зрения разделения абстракций два этих вида кофе-машин сами находятся на разных уровнях: первые предоставляют API более высокого уровня, нежели вторые; следовательно, и «ветка» нашего API, работающая со вторым видом машин, будет более «развесистой».
Следующий шаг, необходимый для отделения уровней абстракции — необходимо понять, какую функциональность нам, собственно, необходимо абстрагировать. Для этого нам необходимо обратиться к задачам, которые решает разработчик на уровне работы с заказами, и понять, какие проблемы у него возникнут в случае отсутствия нашего слоя абстракции.
+
1. Очевидно, что разработчику хочется создавать заказ унифицированным образом — перечислить высокоуровневые параметры заказа (вид напитка, объём и специальные требования, такие как вид сиропа или молока) — и не думать о том, как на конкретной машине исполнить этот заказ.
2. Разработчику надо понимать состояние исполнения — готов ли заказ или нет; если не готов — когда ожидать готовность (и надо ли её ожидать вообще в случае ошибки исполнения).
3. Разработчику нужно уметь соотносить заказ с его положением в пространстве и времени — чтобы показать потребителю, когда и как нужно заказ забрать.
- 4. Наконец, разработчику нужно выполнять атомарные операции — прежде всего, отменять заказ.
+ 4. Наконец, разработчику нужно выполнять атомарные операции — например, отменять заказ.
-Таким образом, наш промежуточный уровень абстракции должен:
- * скрывать параметры создания задания на приготовление напитка — их вычисление должно производиться в нашем коде;
- * обобщать состояние исполнения в единый набор статусов, не зависящий от физического уровня приготовления напитка;
- * связывать параметры заказа (его уникальный идентификатор) с конкретным процессом приготовления напитка;
- * предоставлять атомарный интерфейс логических операций, скрывая за фасадом различия в физической имплементации этих операций.
+Заметим, что API первого типа гораздо ближе к потребностям разработчика, нежели API второго типа. Концепция атомарной «программы» гораздо ближе к удобному для разработчика интерфейсу, нежели работа с сырыми наборами команд и данными сенсоров. В API первого типа мы видим только две проблемы:
+ * отсутствие явного соответствия программ и рецептов; идентификатор программы по-хорошему вообще не нужен при работе с заказами, раз уже есть понятие рецепта;
+ * отсутствие явного статуса готовности.
-Первая из проблем, которую мы видим на данном этапе — это отсутствие на физическом уровне машин второго типа самого понятия «заказ» или «напиток»: машина выполняет какой-то набор операций
+С API второго типа всё гораздо хуже. Главная проблема, которая нас ожидает — отсутствие «памяти» исполняемых действий. API функций и сенсоров полностью stateless; это означает, что мы даже не знаем, кем, когда и в рамках какого заказа была запущена текущая функция.
-Выделение уровней абстракции — прежде всего _логическая_ процедура: как мы объясняем себе и разработчику, из чего состоит наш API. Мы могли бы просто ограничиться выделением секции `task` в ответе `GET /orders/{id}` — или вовсе сказать, что `task` — это просто четверка полей (`ready`, `volume_requested`, `volume_prepared`, `readiness_policy`) и есть. **Абстрагируемая дистанция между сущностями существует объективно**, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни _явно_. Чем неявнее разведены уровни абстракции (или хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API и тем хуже будет написан использующий его код.
+Таким образом, нам нужно внедрить два новых уровня абстракции:
-NB: важно заметить, что с дальнейшей проработкой уровень исполнения, скорее всего, сам должен будет разделиться на два и более уровня, т.к. «задача» по сути — просто сущность-зонтик, связывающая в рамках заказа несколько высокоуровневых сущностей. Идея определения параметров кофе-машины на этапе создания заказов не очень удобна, да и до манипуляции командами кофе-машины и состоянием сенсоров всё ещё далеко с точки зрения абстрагирования. Но мы пока оставим в таком виде, для удобства дальнейшего изложения.
+ 1. Уровень управления исполнением, предоставляющий унифицированный интерфейс к атомарным программам. «Унифицированный интерфейс» в данном случае означает, что, независимо от того, на какого рода кофе-машине готовится заказ, разработчик может рассчитывать на:
+ * единую номенклатуру статусов и других высокоуровневых параметров исполнения (например, ожидаемого времени готовности заказа или возможных ошибок исполнения);
+ * единую номенклатуру доступных методов (например, отмены заказа) и их одинаковое поведение.
+
+ 2. Уровень программы исполнения. Для API первого типа он будет представлять собой просто обёртку над существующим API программ; для API второго типа концепцию «программ» придётся полностью имплементировать нам.
+
+Что это будет означать практически? Разработчик по-прежнему будет создавать заказ, оперируя только высокоуровневыми терминами:
+```
+POST /v1/coffee-machines/orders?machine_id={id}
+{recipe:"lungo","volume":"800ml"}
+```
+
+Имплементация функции `POST /orders` проверит все параметры заказа, заблокирует его стоимость на карте пользователя, сформирует полный запрос на исполнение и обратится к уровню исполнения
+```
+POST /v1/execute
+{
+ "order_id",
+ "coffee_machine",
+ "recipe",
+ "volume_requested": "800ml"
+}
+```
+```
+{
+ "execution_id": <идентификатор исполнения>
+}
+```
+Далее нам нужно подобрать нужную программу исполнения:
+```
+POST /v1/programs/match
+{ "recipe", "coffee-machine" }
+```
+```
+{ "program_id" }
+```
+Наконец, обладая идентификатором нужной программы, мы можем её запустить:
+```
+POST /v1/programs/{id}/run
+{
+ "execution_id",
+ "coffee_machine_id"
+}
+```
+```
+{ "program_run_id" }
+```
+
+Обратите внимание, что во всей этой цепочке вообще никак не участвует тип API кофе-машины — собственно, ровно для этого мы и абстрагировали. Мы могли бы сделать интерфейсы более конкретными, разделив функциональность `run` и `match` для разных API, т.е. ввести раздельные endpoint-ы:
+ * `POST /v1/programs/{api_type}/match`
+ * `POST /v1/programs/{api_type}/{program_id}/run`
+
+Достоинством такого подхода была бы возможность передавать в match и run не унифицированные наборы параметров, а только те, которые имеют значение в контексте указанного типа API. Однако в нашем дизайне API такой необходимости не прослеживается.
+
+_NB_: в имплементации связки `execute` → `match` → `run` можно пойти одним из двух путей:
+ * либо `POST /orders` сама обращается к доступной информации о рецепте и кофе-машине и формирует stateless-запрос, в котором указаны все нужные данные (тип API кофе-машины в частности);
+ * либо в запросе содержатся только идентификаторы, и имплементация методов сами обратятся за нужными данными через какие-то внутренние API.
+Оба варианта имеют право на жизнь; какой из них выбрать — зависит от деталей реализации.
+
+Любопытно, что введённая сущность `match` связывает два уровня абстракции, и тем самым не относится ни к одному из них. Такая ситуация (когда некоторые вспомогательные сущности находятся вне общей иерархии) случается довольно часто.
+
+// TODO
+
+Выделение уровней абстракции — прежде всего _логическая_ процедура: как мы объясняем себе и разработчику, из чего состоит наш API. **Абстрагируемая дистанция между сущностями существует объективно**, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни _явно_. Чем неявнее разведены уровни абстракции (или хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API и тем хуже будет написан использующий его код.
#### Изоляция уровней абстракции