From 7c544d927469fef9596777cc83d567799247fecd Mon Sep 17 00:00:00 2001 From: Sergey Konstantinov Date: Mon, 23 Nov 2020 22:42:04 +0300 Subject: [PATCH] style fix --- docs/API.ru.html | 166 ++++++++++-------- docs/API.ru.pdf | Bin 2812316 -> 2818777 bytes src/ru/clean-copy/01-Введение/06.md | 11 +- .../clean-copy/02-I. Проектирование API/03.md | 85 +++++---- .../clean-copy/02-I. Проектирование API/05.md | 37 ++-- 5 files changed, 162 insertions(+), 137 deletions(-) diff --git a/docs/API.ru.html b/docs/API.ru.html index 3583974..80946a8 100644 --- a/docs/API.ru.html +++ b/docs/API.ru.html @@ -138,15 +138,16 @@ h4, h5 {

Для составных частей сущности, к сожалению, достаточно нейтрального термина нам придумать не удалось, поэтому мы используем слова «поля» и «методы».

Большинство примеров API в общих разделах будут даны в виде JSON-over-HTTP-эндпойтов. Это некоторая условность, которая помогает описать концепции, как нам кажется, максимально понятно. Вместо GET /v1/orders вполне может быть вызов метода orders.get(), локальный или удалённый; вместо JSON может быть любой другой формат данных. Смысл утверждений от этого не меняется.

Рассмотрим следующую запись:

-
POST /v1/bucket/{id}/some-resource
+
// Описание метода
+POST /v1/bucket/{id}/some-resource
 {
   …
   // Это однострочный комментарий
   "some_parameter": "value",
   …
 }
-
-
{
+→
+{
   /* А это многострочный
      комментарий */
   "operation_id"
@@ -158,11 +159,9 @@ h4, h5 {
 
  • в качестве тела запроса передаётся 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 в контексте главы не упоминается и перепутать не с чем).

    I. Проектирование API

    Глава 7. Пирамида контекстов API

    +

    Тело ответа или запроса может быть опущено, если в контексте обсуждаемого вопроса его содержание не имеет значения.

    +

    Для упрощения возможна сокращенная запись вида: POST /v1/bucket/{id}/some-resource {…,"some_parameter",…}{ "operation_id" }; тело запроса и/или ответа может опускаться аналогично полной записи.

    +

    Чтобы сослаться на это описание будут использоваться выражения типа «метод POST /v1/bucket/{id}/some-resource» или, для простоты, «метод some-resource» или «метод bucket/some-resource» (если никаких других some-resource в контексте главы не упоминается и перепутать не с чем).

    I. Проектирование API

    Глава 7. Пирамида контекстов API

    Подход, который мы используем для проектирования, состоит из четырёх шагов:

    • определение области применения;
    • @@ -214,11 +213,9 @@ h4, h5 {

      Прежде чем переходить к теории, следует чётко сформулировать, зачем нужны уровни абстракции и каких целей мы хотим достичь их выделением.

      Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим?

        -
      1. Непосредственно состояние кофе-машины и шаги приготовления кофе. Температура, давление, объём воды.
      2. -
      3. У кофе есть мета-характеристики: сорт, вкус, вид напитка.
      4. -
      5. Мы готовим с помощью нашего API заказ — один или несколько стаканов кофе с определенной стоимостью.
      6. -
      7. Наши кофе-машины как-то распределены в пространстве (и времени).
      8. -
      9. Кофе-машина принадлежит какой-то сети кофеен, каждая из которых обладает какой-то айдентикой и специальными возможностями.
      10. +
      11. Мы готовим с помощью нашего API заказ — один или несколько стаканов кофе — и взымаем за это плату.
      12. +
      13. Каждый стакан кофе приготовлен по определённому рецепту, что подразумевает наличие разных ингредиентов и последовательности выполнения шагов приготовления.
      14. +
      15. Напиток готовится на конкретной физической кофе-машине, располагающейся в какой-то точке пространства.

      Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы прежде всего стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей:

        @@ -227,28 +224,35 @@ h4, h5 {
      1. Поддержание интероперабельности. Правильно выделенные низкоуровневые абстракции позволят нам адаптировать наше API к другим платформам, не меняя высокоуровневый интерфейс.

      Допустим, мы имеем следующий интерфейс:

      -
        -
      • GET /v1/recipes/lungo
        -— возвращает рецепт лунго;
      • -
      • POST /v1/coffee-machines/orders?machine_id={id}
        -{recipe:"lungo"}
        -— размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа;
      • -
      • GET /v1/orders?order_id={id}
        -— возвращает состояние заказа;
      • -
      +
        // возвращает рецепт лунго
      +  GET /v1/recipes/lungo
      +
      +
        // размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа
      +  POST /v1/coffee-machines/orders?machine_id={id}
      +  {
      +    "recipe": "lungo"
      +  }
      +
      +
        // возвращает состояние заказа
      +  GET /v1/orders?order_id={id}
      +

      И зададимся вопросом, каким образом разработчик определит, что заказ клиента готов. Допустим, мы сделаем так: добавим в рецепт лунго эталонный объём, а в состояние заказа — количество уже налитого кофе. Тогда разработчику нужно будет проверить совпадение этих двух цифр, чтобы убедиться, что кофе готов.

      -

      Такое решение выглядит интуитивно плохим, и это действительно так: оно нарушает все вышеперечисленные принципы:

      +

      Такое решение выглядит интуитивно плохим, и это действительно так: оно нарушает все вышеперечисленные принципы.

      1. Для решения задачи «заказать лунго» разработчику нужно обратиться к сущности «рецепт» и выяснить, что у каждого рецепта есть объём. Далее, нужно принять концепцию, что приготовление кофе заканчивается в тот момент, когда объём сравнялся с эталонным. Нет никакого способа об этой конвенции догадаться: она неочевидна и её нужно найти в документации. При этом никакой пользы для разработчика в этом знании нет.

      2. Мы автоматически получаем проблемы, если захотим варьировать размер кофе. Допустим, в какой-то момент мы захотим представить пользователю выбор, сколько конкретно миллилитров лунго он желает. Тогда нам придётся проделать один из следующих трюков:

        • или мы фиксируем список допустимых объёмов и заводим фиктивные рецепты типа /recipes/small-lungo, recipes/large-lungo. Почему фиктивные? Потому что рецепт один и тот же, меняется только объём. Нам придётся либо тиражировать одинаковые рецепты, отличающиеся только объёмом, либо вводить какое-то «наследование» рецептов, чтобы можно было указать базовый рецепт и только переопределить объём;
        • -
        • или мы модифицируем интерфейс, объявляя объём кофе, указанный в рецепте, значением по умолчанию; при размещении заказа мы разрешаем указать объём, отличный от эталонного:
          -POST /v1/coffee-machines/orders?machine_id={id}
          -{recipe:"lungo","volume":"800ml"}
          -Для таких кофе произвольного объёма нужно будет получать требуемый объём не из GET /v1/recipes, а из GET /v1/orders. Сделав так, мы сразу получаем клубок из связанных проблем:
        • -
        • разработчик, которому придётся поддержать эту функциональность, имеет высокие шансы сделать ошибку: добавив поддержку произвольного объёма кофе в код, работающий с POST /v1/coffee-machines/orders нужно не забыть переписать код проверки готовности заказа;
        • -
        • мы получим классическую ситуацию, когда одно и то же поле (объём кофе) значит разные вещи в разных интерфейсах. В GET /v1/recipes поле «объём» теперь значит «объём, который будет запрошен, если не передать его явно в POST /v1/coffee-machines/orders»; переименовать его в «объём по умолчанию» уже не получится, с этой проблемой теперь придётся жить.
      3. +
      4. или мы модифицируем интерфейс, объявляя объём кофе, указанный в рецепте, значением по умолчанию; при размещении заказа мы разрешаем указать объём, отличный от эталонного:
    +
      POST /v1/coffee-machines/orders?machine_id={id}
    +  {
    +    "recipe":"lungo",
    +    "volume":"800ml"
    +  }
    +
    +

    Для таких кофе произвольного объёма нужно будет получать требуемый объём не из GET /v1/recipes, а из GET /v1/orders. Сделав так, мы сразу получаем клубок из связанных проблем: + * разработчик, которому придётся поддержать эту функциональность, имеет высокие шансы сделать ошибку: добавив поддержку произвольного объёма кофе в код, работающий с POST /v1/coffee-machines/orders нужно не забыть переписать код проверки готовности заказа; + * мы получим классическую ситуацию, когда одно и то же поле (объём кофе) значит разные вещи в разных интерфейсах. В GET /v1/recipes поле «объём» теперь значит «объём, который будет запрошен, если не передать его явно в POST /v1/coffee-machines/orders»; переименовать его в «объём по умолчанию» уже не получится, с этой проблемой теперь придётся жить.

  • Вся эта схема полностью неработоспособна, если разные модели кофе-машин производят лунго разного объёма. Для решения задачи «объём лунго зависит от вида машины» нам придётся сделать совсем неприятную вещь: сделать рецепт зависимым от id машины. Тем самым мы начнём активно смешивать уровни абстракции: одной частью нашего API (рецептов) станет невозможно пользоваться без другой части (информации о кофе-машинах). Что немаловажно, от разработчиков потребуется изменить логику своего приложения: если раньше они могли предлагать сначала выбрать объём, а потом кофе-машину, то теперь им придётся полностью изменить этот шаг.

  • Хорошо, допустим, мы поняли, как сделать плохо. Но как же тогда сделать хорошо? Разделение уровней абстракции должно происходить вдоль трёх направлений:

    @@ -290,8 +294,9 @@ h4, h5 {

    Предположим, для большей конкретности, что эти два класса устройств поставляются вот с таким физическим API: