mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-05-31 22:09:37 +02:00
Several misspellings fixed
This commit is contained in:
parent
b543b9c0d2
commit
0ef9a9bd9f
@ -10,7 +10,7 @@
|
||||
```
|
||||
// Описание метода
|
||||
POST /v1/bucket/{id}/some-resource
|
||||
X-Idempotency-Token: <токен идемпотентости>
|
||||
X-Idempotency-Token: <токен идемпотентности>
|
||||
{
|
||||
…
|
||||
// Это однострочный комментарий
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
* Возможно, мы хотим решить проблему выбора и знания? Чтобы человек наиболее полно знал о доступных ему здесь и сейчас опциях.
|
||||
* Возможно, мы оптимизируем время ожидания? Чтобы человеку не пришлось ждать, пока его заказ готовится.
|
||||
* Возможно, мы хотим минимизировать ошибки? Чтобы человек получил именно то, что хотел заказать, не потеряв информацию при разговорном общении либо при настройке незнакомого интерфейса кофе-машины.
|
||||
* Возможно, мы хотим минимизировать ошибки? Чтобы человек получил именно то, что хотел заказать, не потеряв информацию при разговорном общении либо при настройке незнакомого интерфейса кофемашины.
|
||||
|
||||
Вопрос «зачем» — самый важный из тех вопросов, которые вы должны задавать себе. Не только глобально в отношении целей всего проекта, но и локально в отношении каждого кусочка функциональности. **Если вы не можете коротко и понятно ответить на вопрос «зачем эта сущность нужна» — значит, она не нужна**.
|
||||
|
||||
@ -24,11 +24,11 @@
|
||||
|
||||
2. Правда ли решаемая проблема существует? Действительно ли мы наблюдаем неравномерную загрузку кофейных автоматов по утрам? Правда ли люди страдают от того, что не могут найти поблизости нужный им латте с ореховым сиропом? Действительно ли людям важны те минуты, которые они теряют, стоя в очередях?
|
||||
|
||||
3. Действительно ли мы обладаем достаточным ресурсом, чтобы решить эту проблему? Есть ли у нас доступ к достаточному количеству кофе-машин и клиентов, чтобы обеспечить работоспособность системы?
|
||||
3. Действительно ли мы обладаем достаточным ресурсом, чтобы решить эту проблему? Есть ли у нас доступ к достаточному количеству кофемашин и клиентов, чтобы обеспечить работоспособность системы?
|
||||
|
||||
4. Наконец, правда ли мы решим проблему? Как мы поймём, что оптимизировали перечисленные факторы?
|
||||
|
||||
На все эти вопросы, в общем случае, простого ответа нет. В идеале ответы на эти вопросы должны даваться с цифрами в руках. Сколько конкретно времени тратится неоптимально, и какого значения мы рассчитываем добиться, располагая какой плотностью кофе-машин? Заметим также, что в реальной жизни просчитать такого рода цифры можно в основном для проектов, которые пытаются влезть на уже устоявшийся рынок; если вы пытаетесь сделать что-то новое, то, вероятно, вам придётся ориентироваться в основном на свою интуицию.
|
||||
На все эти вопросы, в общем случае, простого ответа нет. В идеале ответы на эти вопросы должны даваться с цифрами в руках. Сколько конкретно времени тратится неоптимально, и какого значения мы рассчитываем добиться, располагая какой плотностью кофемашин? Заметим также, что в реальной жизни просчитать такого рода цифры можно в основном для проектов, которые пытаются влезть на уже устоявшийся рынок; если вы пытаетесь сделать что-то новое, то, вероятно, вам придётся ориентироваться в основном на свою интуицию.
|
||||
|
||||
#### Почему API?
|
||||
|
||||
|
@ -6,9 +6,9 @@
|
||||
|
||||
Вспомним, что программный продукт — это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят — тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим?
|
||||
|
||||
1. Мы готовим с помощью нашего API *заказ* — один или несколько стаканов кофе — и взымаем за это плату.
|
||||
1. Мы готовим с помощью нашего API *заказ* — один или несколько стаканов кофе — и взимаем за это плату.
|
||||
2. Каждый стакан кофе приготовлен по определённому *рецепту*, что подразумевает наличие разных ингредиентов и последовательности выполнения шагов приготовления.
|
||||
3. Напиток готовится на конкретной физической *кофе-машине*, располагающейся в какой-то точке пространства.
|
||||
3. Напиток готовится на конкретной физической *кофемашине*, располагающейся в какой-то точке пространства.
|
||||
|
||||
Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы, прежде всего, стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей.
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
GET /v1/recipes/lungo
|
||||
```
|
||||
```
|
||||
// размещает на указанной кофе-машине
|
||||
// размещает на указанной кофемашине
|
||||
// заказ на приготовление лунго
|
||||
// и возвращает идентификатор заказа
|
||||
POST /v1/orders
|
||||
@ -62,22 +62,22 @@ POST /v1/orders
|
||||
* разработчик, которому придётся поддержать эту функциональность, имеет высокие шансы сделать ошибку: добавив поддержку произвольного объёма кофе в код, работающий с `POST /v1/orders` нужно не забыть переписать код проверки готовности заказа;
|
||||
* мы получим классическую ситуацию, когда одно и то же поле (объём кофе) значит разные вещи в разных интерфейсах. В `GET /v1/recipes` поле «объём» теперь значит «объём, который будет запрошен, если не передать его явно в `POST /v1/orders`»; переименовать его в «объём по умолчанию» уже не получится, с этой проблемой теперь придётся жить.
|
||||
|
||||
**В-третьих**, вся эта схема полностью неработоспособна, если разные модели кофе-машин производят лунго разного объёма. Для решения задачи «объём лунго зависит от вида машины» нам придётся сделать совсем неприятную вещь: сделать рецепт зависимым от id машины. Тем самым мы начнём активно смешивать уровни абстракции: одной частью нашего API (рецептов) станет невозможно пользоваться без другой части (информации о кофе-машинах). Что немаловажно, от разработчиков потребуется изменить логику своего приложения: если раньше они могли предлагать сначала выбрать объём, а потом кофе-машину, то теперь им придётся полностью изменить этот шаг.
|
||||
**В-третьих**, вся эта схема полностью неработоспособна, если разные модели кофемашин производят лунго разного объёма. Для решения задачи «объём лунго зависит от вида машины» нам придётся сделать совсем неприятную вещь: сделать рецепт зависимым от id машины. Тем самым мы начнём активно смешивать уровни абстракции: одной частью нашего API (рецептов) станет невозможно пользоваться без другой части (информации о кофемашинах). Что немаловажно, от разработчиков потребуется изменить логику своего приложения: если раньше они могли предлагать сначала выбрать объём, а потом кофемашину, то теперь им придётся полностью изменить этот шаг.
|
||||
|
||||
Хорошо, допустим, мы поняли, как сделать плохо. Но как же тогда сделать *хорошо*? Разделение уровней абстракции должно происходить вдоль трёх направлений:
|
||||
|
||||
1. От сценариев использования к их внутренней реализации: высокоуровневые сущности и номенклатура их методов должны напрямую отражать сценарии использования API; низкоуровневый — отражать декомпозицию сценариев на составные части.
|
||||
|
||||
2. От терминов предметной области пользователя к терминам предметной области исходных данных — в нашем случае от высокоуровневых понятий «рецепт», «заказ», «кофейня» к низкоуровневым «температура напитка» и «координаты кофе-машины»
|
||||
2. От терминов предметной области пользователя к терминам предметной области исходных данных — в нашем случае от высокоуровневых понятий «рецепт», «заказ», «кофейня» к низкоуровневым «температура напитка» и «координаты кофемашины»
|
||||
|
||||
3. Наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к «сырым» — в нашем случае от «лунго» и «сети кофеен "Ромашка"» — к сырым байтовый данным, описывающим состояние кофе-машины марки «Доброе утро» в процессе приготовления напитка.
|
||||
3. Наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к «сырым» — в нашем случае от «лунго» и «сети кофеен "Ромашка"» — к сырым байтовый данным, описывающим состояние кофемашины марки «Доброе утро» в процессе приготовления напитка.
|
||||
|
||||
Чем дальше находятся друг от друга программные контексты, которые соединяет наш API — тем более глубокая иерархия сущностей должна получиться у нас в итоге.
|
||||
|
||||
В нашем примере с определением готовности кофе мы явно пришли к тому, что нам требуется промежуточный уровень абстракции:
|
||||
|
||||
* с одной стороны, «заказ» не должен содержать информацию о датчиках и сенсорах кофе-машины;
|
||||
* с другой стороны, кофе-машина не должна хранить информацию о свойствах заказа (да и вероятно её API такой возможности и не предоставляет).
|
||||
* с одной стороны, «заказ» не должен содержать информацию о датчиках и сенсорах кофемашины;
|
||||
* с другой стороны, кофемашина не должна хранить информацию о свойствах заказа (да и вероятно её API такой возможности и не предоставляет).
|
||||
|
||||
Наивный подход в такой ситуации — искусственно ввести некий промежуточный уровень абстракции, «передаточное звено», который переформулирует задачи одного уровня абстракции в другой. Например, введём сущность `task` вида:
|
||||
```
|
||||
@ -91,7 +91,7 @@ POST /v1/orders
|
||||
"status": "executing",
|
||||
"operations": [
|
||||
// описание операций, запущенных на
|
||||
// физической кофе-машине
|
||||
// физической кофемашине
|
||||
]
|
||||
}
|
||||
…
|
||||
@ -100,11 +100,11 @@ POST /v1/orders
|
||||
|
||||
Мы называем этот подход «наивным» не потому, что он неправильный; напротив, это вполне логичное решение «по умолчанию», если вы на данном этапе ещё не знаете или не понимаете, как будет выглядеть ваш API. Проблема его в том, что он умозрительный: он не добавляет понимания того, как устроена предметная область.
|
||||
|
||||
Хороший разработчик в нашем примере должен спросить: хорошо, а какие вообще говоря существуют варианты? Как можно определять готовность напитка? Если вдруг окажется, что сравнение объёмов — единственный способ определения готовности во всех без исключения кофе-машинах, то почти все рассуждения выше — неверны: можно совершенно спокойно включать в интерфейсы определение готовности кофе по объёму, т.к. никакого другого и не существует. Прежде, чем что-то абстрагировать — надо представлять, *что* мы, собственно, абстрагируем.
|
||||
Хороший разработчик в нашем примере должен спросить: хорошо, а какие вообще говоря существуют варианты? Как можно определять готовность напитка? Если вдруг окажется, что сравнение объёмов — единственный способ определения готовности во всех без исключения кофемашинах, то почти все рассуждения выше — неверны: можно совершенно спокойно включать в интерфейсы определение готовности кофе по объёму, т.к. никакого другого и не существует. Прежде, чем что-то абстрагировать — надо представлять, *что* мы, собственно, абстрагируем.
|
||||
|
||||
Для нашего примера допустим, что мы сели изучать спецификации API кофе-машин и выяснили, что существует принципиально два класса устройств:
|
||||
* кофе-машины с предустановленными программами, которые умеют готовить заранее прошитые N видов напитков, и мы можем управлять только какими-то параметрами напитка (скажем, объёмом напитка, вкусом сиропа и видом молока); у таких машин отсутствует доступ к внутренним функциям и датчикам, но зато машина умеет через API сама отдавать статус приготовления напитка;
|
||||
* кофе-машины с предустановленными функциями типа «смолоть такой-то объём кофе», «пролить N миллилитров воды», «взбить молочную пену» и т.д.: у таких машин отсутствует понятие «программа приготовления», но есть доступ к микрокомандам и датчикам.
|
||||
Для нашего примера допустим, что мы сели изучать спецификации API кофемашин и выяснили, что существует принципиально два класса устройств:
|
||||
* кофемашины с предустановленными программами, которые умеют готовить заранее прошитые N видов напитков, и мы можем управлять только какими-то параметрами напитка (скажем, объёмом напитка, вкусом сиропа и видом молока); у таких машин отсутствует доступ к внутренним функциям и датчикам, но зато машина умеет через API сама отдавать статус приготовления напитка;
|
||||
* кофемашины с предустановленными функциями типа «смолоть такой-то объём кофе», «пролить N миллилитров воды», «взбить молочную пену» и т.д.: у таких машин отсутствует понятие «программа приготовления», но есть доступ к микрокомандам и датчикам.
|
||||
|
||||
Предположим, для большей конкретности, что эти два класса устройств поставляются вот с таким физическим API.
|
||||
|
||||
@ -148,7 +148,7 @@ POST /v1/orders
|
||||
GET /execution/status
|
||||
```
|
||||
|
||||
**NB**. На всякий случай отметим, что данный API нарушает множество описанных нами принципов проектирования, начиная с отсутствия версионирования; он приведен в таком виде по двум причинам: (1) чтобы мы могли показать, как спроектировать API более удачно; (2) скорее всего, в реальной жизни вы получите именно такой API от производителей кофе-машин, и это ещё довольно вменяемый вариант.
|
||||
**NB**. На всякий случай отметим, что данный API нарушает множество описанных нами принципов проектирования, начиная с отсутствия версионирования; он приведен в таком виде по двум причинам: (1) чтобы мы могли показать, как спроектировать API более удачно; (2) скорее всего, в реальной жизни вы получите именно такой API от производителей кофемашин, и это ещё довольно вменяемый вариант.
|
||||
|
||||
* Машины с предустановленными функциями:
|
||||
```
|
||||
@ -203,7 +203,7 @@ POST /v1/orders
|
||||
|
||||
**NB**. Пример нарочно сделан умозрительным для моделирования ситуации, описанной в начале главы: для определения готовности напитка нужно сличить объём налитого с эталоном.
|
||||
|
||||
Теперь картина становится более явной: нам нужно абстрагировать работу с кофе-машиной так, чтобы наш «уровень исполнения» в API предоставлял общие функции (такие, как определение готовности напитка) в унифицированном виде. Важно отметить, что с точки зрения разделения абстракций два этих вида кофе-машин сами находятся на разных уровнях: первые предоставляют API более высокого уровня, нежели вторые; следовательно, и «ветка» нашего API, работающая со вторым видом машин, будет более «развесистой».
|
||||
Теперь картина становится более явной: нам нужно абстрагировать работу с кофемашиной так, чтобы наш «уровень исполнения» в API предоставлял общие функции (такие, как определение готовности напитка) в унифицированном виде. Важно отметить, что с точки зрения разделения абстракций два этих вида кофемашин сами находятся на разных уровнях: первые предоставляют API более высокого уровня, нежели вторые; следовательно, и «ветка» нашего API, работающая со вторым видом машин, будет более «развесистой».
|
||||
|
||||
Следующий шаг, необходимый для отделения уровней абстракции — необходимо понять, какую функциональность нам, собственно, необходимо абстрагировать. Для этого нам необходимо обратиться к задачам, которые решает разработчик на уровне работы с заказами, и понять, какие проблемы у него возникнут в случае отсутствия нашего слоя абстракции.
|
||||
|
||||
@ -220,7 +220,7 @@ POST /v1/orders
|
||||
|
||||
Таким образом, нам нужно внедрить два новых уровня абстракции.
|
||||
|
||||
1. Уровень управления исполнением, предоставляющий унифицированный интерфейс к атомарным программам. «Унифицированный интерфейс» в данном случае означает, что, независимо от того, на какого рода кофе-машине готовится заказ, разработчик может рассчитывать на:
|
||||
1. Уровень управления исполнением, предоставляющий унифицированный интерфейс к атомарным программам. «Унифицированный интерфейс» в данном случае означает, что, независимо от того, на какого рода кофемашине готовится заказ, разработчик может рассчитывать на:
|
||||
* единую номенклатуру статусов и других высокоуровневых параметров исполнения (например, ожидаемого времени готовности заказа или возможных ошибок исполнения);
|
||||
* единую номенклатуру доступных методов (например, отмены заказа) и их одинаковое поведение.
|
||||
|
||||
@ -262,12 +262,12 @@ POST /v1/programs/{id}/run
|
||||
{ "program_run_id" }
|
||||
```
|
||||
|
||||
Обратите внимание, что во всей этой цепочке вообще никак не участвует тип API кофе-машины — собственно, ровно для этого мы и абстрагировали. Мы могли бы сделать интерфейсы более конкретными, разделив функциональность `run` и `match` для разных API, т.е. ввести раздельные endpoint-ы:
|
||||
Обратите внимание, что во всей этой цепочке вообще никак не участвует тип API кофемашины — собственно, ровно для этого мы и абстрагировали. Мы могли бы сделать интерфейсы более конкретными, разделив функциональность `run` и `match` для разных API, т.е. ввести раздельные endpoint-ы:
|
||||
* `POST /v1/program-matcher/{api_type}`
|
||||
* `POST /v1/programs/{api_type}/{program_id}/run`
|
||||
|
||||
Достоинством такого подхода была бы возможность передавать в match и run не унифицированные наборы параметров, а только те, которые имеют значение в контексте указанного типа API. Однако в нашем дизайне API такой необходимости не прослеживается. Обработчик `run` сам может извлечь нужные параметры из мета-информации о программе и выполнить одно из двух действий:
|
||||
* вызвать `POST /execute` физического API кофе-машины с передачей внутреннего идентификатора программы — для машин, поддерживающих API первого типа;
|
||||
* вызвать `POST /execute` физического API кофемашины с передачей внутреннего идентификатора программы — для машин, поддерживающих API первого типа;
|
||||
* инициировать создание рантайма для работы с API второго типа.
|
||||
|
||||
Уровень рантаймов API второго типа, исходя из общих соображений, будет скорее всего непубличным, и мы плюс-минус свободны в его имплементации. Самым простым решением будет реализовать виртуальную state-машину, которая создаёт «рантайм» (т.е. stateful контекст исполнения) для выполнения программы и следит за его состоянием.
|
||||
@ -319,7 +319,7 @@ POST /v1/runtimes
|
||||
```
|
||||
|
||||
**NB**: в имплементации связки `orders` → `match` → `run` → `runtimes` можно пойти одним из двух путей:
|
||||
* либо обработчик `POST /orders` сам обращается к доступной информации о рецепте, кофе-машине и программе и формирует stateless-запрос, в котором указаны все нужные данные (тип API кофе-машины и список команд в частности);
|
||||
* либо обработчик `POST /orders` сам обращается к доступной информации о рецепте, кофемашине и программе и формирует stateless-запрос, в котором указаны все нужные данные (тип API кофемашины и список команд в частности);
|
||||
* либо в запросе содержатся только идентификаторы, и следующие обработчики в цепочке сами обратятся за нужными данными через какие-то внутренние API.
|
||||
|
||||
Оба варианта имеют право на жизнь; какой из них выбрать зависит от деталей реализации.
|
||||
@ -331,8 +331,8 @@ POST /v1/runtimes
|
||||
Вернёмся к нашему примеру. Каким образом будет работать операция получения статуса заказа? Для получения статуса будет выполнена следующая цепочка вызовов:
|
||||
* пользователь вызовет метод `GET /v1/orders`;
|
||||
* обработчик `orders` выполнит операции своего уровня ответственности (проверку авторизации, в частности), найдёт идентификатор `program_run_id` и обратится к API программ `runs/{program_run_id}`;
|
||||
* обработчик `runs` в свою очередь выполнит операции своего уровня (в частности, проверит тип API кофе-машины) и в зависимости от типа API пойдёт по одной из двух веток исполнения:
|
||||
* либо вызовет `GET /execution/status` физического API кофе-машины, получит объём кофе и сличит с эталонным;
|
||||
* обработчик `runs` в свою очередь выполнит операции своего уровня (в частности, проверит тип API кофемашины) и в зависимости от типа API пойдёт по одной из двух веток исполнения:
|
||||
* либо вызовет `GET /execution/status` физического API кофемашины, получит объём кофе и сличит с эталонным;
|
||||
* либо обратится к `GET /v1/runtimes/{runtime_id}`, получит `state.status` и преобразует его к статусу заказа;
|
||||
* в случае API второго типа цепочка продолжится: обработчик `GET /runtimes` обратится к физическому API `GET /sensors` и произведёт ряд манипуляций: сопоставит объём стакана / молотого кофе / налитой воды с запрошенным и при необходимости изменит состояние и статус.
|
||||
|
||||
@ -350,8 +350,8 @@ POST /v1/runtimes
|
||||
* проверит авторизацию;
|
||||
* решит денежные вопросы — нужно ли делать рефанд;
|
||||
* найдёт идентификатор `program_run_id` и обратится к обработчику `runs/{program_run_id}/cancel`;
|
||||
* обработчик `runs/cancel` произведёт операции своего уровня (в частности, установит тип API кофе-машины) и в зависимости от типа API пойдёт по одной из двух веток исполнения:
|
||||
* либо вызовет `POST /execution/cancel` физического API кофе-машины;
|
||||
* обработчик `runs/cancel` произведёт операции своего уровня (в частности, установит тип API кофемашины) и в зависимости от типа API пойдёт по одной из двух веток исполнения:
|
||||
* либо вызовет `POST /execution/cancel` физического API кофемашины;
|
||||
* либо вызовет `POST /v1/runtimes/{id}/terminate`;
|
||||
* во втором случае цепочка продолжится, обработчик `terminate` изменит внутреннее состояние:
|
||||
* изменит `resolution` на `"terminated"`
|
||||
@ -364,9 +364,9 @@ POST /v1/runtimes
|
||||
* при этом на физическом уровне API второго типа «отмена» как таковая не существует: «отмена» — это исполнение команды `discard_cup`, которая на этом уровне абстракции ничем не отличается от любых других команд.
|
||||
Промежуточный уровень абстракции как раз необходим для того, чтобы переход между «отменами» разных уровней произошёл гладко, без необходимости перепрыгивания через уровни абстракции.
|
||||
|
||||
2. С точки зрения верхнеуровневого API отмена заказа является терминальным действием, т.е. никаких последующих операций уже быть не может; а с точки зрения низкоуровневого API обработка заказа продолжается, т.к. нужно дождаться, когда стакан будет утилизирован, и после этого освободить кофе-машину (т.е. разрешить создание новых рантаймов на ней). Это вторая задача для уровня исполнения: связывать оба статуса, внешний (заказ отменён) и внутренний (исполнение продолжается).
|
||||
2. С точки зрения верхнеуровневого API отмена заказа является терминальным действием, т.е. никаких последующих операций уже быть не может; а с точки зрения низкоуровневого API обработка заказа продолжается, т.к. нужно дождаться, когда стакан будет утилизирован, и после этого освободить кофемашину (т.е. разрешить создание новых рантаймов на ней). Это вторая задача для уровня исполнения: связывать оба статуса, внешний (заказ отменён) и внутренний (исполнение продолжается).
|
||||
|
||||
Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы он выполнял свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой.
|
||||
Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы он выполнял свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофемашины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой.
|
||||
|
||||
Выделение уровней абстракции, прежде всего, _логическая_ процедура: как мы объясняем себе и разработчику, из чего состоит наш API. **Абстрагируемая дистанция между сущностями существует объективно**, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни _явно_. Чем более неявно разведены (или, хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API, и тем хуже будет написан использующий его код.
|
||||
|
||||
@ -380,8 +380,8 @@ POST /v1/runtimes
|
||||
|
||||
1. Данные с сенсоров — объёмы кофе / воды / стакана. Это низший из доступных нам уровней данных, здесь мы не можем ничего изменить или переформулировать.
|
||||
|
||||
2. Непрерывный поток данных сенсоров мы преобразуем в дискретные статусы исполнения команд, вводя в него понятия, не существующие в предметной области. API кофе-машины не предоставляет нам понятий «кофе наливается» или «стакан ставится» — это наше программное обеспечение трактует поступающие потоки данных от сенсоров, вводя новые понятия: если наблюдаемый объём (кофе или воды) меньше целевого — значит, процесс не закончен; если объём достигнут — значит, необходимо сменить статус исполнения и выполнить следующее действие.
|
||||
Важно отметить, что мы не просто вычисляем какие-то новые параметры из имеющихся данных сенсоров: мы сначала создаём новый кортеж данных более высокого уровня — «программа исполнения» как последовательность шагов и условий — и инициализируем его начальные значения. Без этого контекста определить, что собственно происходит с кофе-машиной невозможно.
|
||||
2. Непрерывный поток данных сенсоров мы преобразуем в дискретные статусы исполнения команд, вводя в него понятия, не существующие в предметной области. API кофемашины не предоставляет нам понятий «кофе наливается» или «стакан ставится» — это наше программное обеспечение трактует поступающие потоки данных от сенсоров, вводя новые понятия: если наблюдаемый объём (кофе или воды) меньше целевого — значит, процесс не закончен; если объём достигнут — значит, необходимо сменить статус исполнения и выполнить следующее действие.
|
||||
Важно отметить, что мы не просто вычисляем какие-то новые параметры из имеющихся данных сенсоров: мы сначала создаём новый кортеж данных более высокого уровня — «программа исполнения» как последовательность шагов и условий — и инициализируем его начальные значения. Без этого контекста определить, что собственно происходит с кофемашиной невозможно.
|
||||
|
||||
3. Обладая логическими данными о состоянии исполнения программы, мы можем (вновь через создание нового, более высокоуровневого контекста данных!) свести данные от двух типов API к единому формату исполнения операции создания напитка и её логических параметров: целевой рецепт, объём, статус готовности.
|
||||
|
||||
@ -393,7 +393,7 @@ POST /v1/runtimes
|
||||
|
||||
2. На уровне исполнения мы обращаемся к данным уровня заказа и создаём более низкоуровневый контекст: программа исполнения в виде последовательности шагов, их параметров и условий перехода от одного шага к другому и начальное состояние.
|
||||
|
||||
3. На уровне рантайма мы обращаемся к целевым значениям (какую операцию выполнить и какой целевой объём) и преобразуем их в набор микрокоманд API кофе-машины и набор статусов исполнения каждой команды.
|
||||
3. На уровне рантайма мы обращаемся к целевым значениям (какую операцию выполнить и какой целевой объём) и преобразуем их в набор микрокоманд API кофемашины и набор статусов исполнения каждой команды.
|
||||
|
||||
Если обратиться к описанному в начале главы «плохому» решению (предполагающему самостоятельное определение факта готовности заказа разработчиком), то мы увидим, что и с точки зрения потоков данных происходит смешение понятий:
|
||||
* с одной стороны, в контексте заказа оказываются данные (объём кофе), «просочившиеся» откуда-то с физического уровня; тем самым, уровни абстракции непоправимо смешиваются без возможности их разделить;
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
Теперь нам необходимо определить ответственность каждой сущности: в чём смысл её существования в рамках нашего API, какие действия можно выполнять с самой сущностью, а какие — делегировать другим объектам. Фактически, нам нужно применить «зачем-принцип» к каждой отдельной сущности нашего API.
|
||||
|
||||
Для этого нам нужно пройти по нашему API и сформулировать в терминах предметной области, что представляет из себя каждый объект. Напомню, что из концепции уровней абстракции следует, что каждый уровень иерархии — это некоторая собственная промежуточная предметная область, ступенька, по которой мы переходим от описания задачи в терминах одного связываемого контекста («заказанный пользователем лунго») к описанию в терминах второго («задание кофе-машине на выполнение указанной программы»).
|
||||
Для этого нам нужно пройти по нашему API и сформулировать в терминах предметной области, что представляет из себя каждый объект. Напомню, что из концепции уровней абстракции следует, что каждый уровень иерархии — это некоторая собственная промежуточная предметная область, ступенька, по которой мы переходим от описания задачи в терминах одного связываемого контекста («заказанный пользователем лунго») к описанию в терминах второго («задание кофемашине на выполнение указанной программы»).
|
||||
|
||||
В нашем умозрительном примере получится примерно так:
|
||||
|
||||
@ -19,21 +19,21 @@
|
||||
* получать;
|
||||
* отменять.
|
||||
* Рецепт `recipe` — описывает «идеальную модель» вида кофе, его потребительские свойства. Рецепт в данном контексте для нас неизменяемая сущность, которую можно только просмотреть и выбрать.
|
||||
* Кофе-машина `coffee-machine` — модель объекта реального мира. Из описания кофе-машины мы, в частности, должны извлечь её положение в пространстве и предоставляемые опции (о чём подробнее поговорим ниже).
|
||||
* Кофемашина `coffee-machine` — модель объекта реального мира. Из описания кофемашины мы, в частности, должны извлечь её положение в пространстве и предоставляемые опции (о чём подробнее поговорим ниже).
|
||||
2. Сущности уровня управления исполнением (те сущности, работая с которыми, можно непосредственно исполнить заказ).
|
||||
* Программа `program` — описывает некоторый план исполнения для конкретной кофе-машины. Программы можно только просмотреть.
|
||||
* Селектор программ `programs/matcher` — позволяет связать рецепт и программу исполнения, т.е. фактически выяснить набор данных, необходимых для приготовления конкретного рецепта на конкретной кофе-машине. Селектор работает только на выбор нужной программы.
|
||||
* Запуск программы `programs/run` — конкретный факт исполнения программы на конкретной кофе-машине. Запуски можно:
|
||||
* Программа `program` — описывает некоторый план исполнения для конкретной кофемашины. Программы можно только просмотреть.
|
||||
* Селектор программ `programs/matcher` — позволяет связать рецепт и программу исполнения, т.е. фактически выяснить набор данных, необходимых для приготовления конкретного рецепта на конкретной кофемашине. Селектор работает только на выбор нужной программы.
|
||||
* Запуск программы `programs/run` — конкретный факт исполнения программы на конкретной кофемашине. Запуски можно:
|
||||
* инициировать (создавать);
|
||||
* проверять состояние запуска;
|
||||
* отменять.
|
||||
3. Сущности уровня программ исполнения (те сущности, работая с которыми, можно непосредственно управлять состоянием кофе-машины через API второго типа).
|
||||
3. Сущности уровня программ исполнения (те сущности, работая с которыми, можно непосредственно управлять состоянием кофемашины через API второго типа).
|
||||
* Рантайм `runtime` — контекст исполнения программы, т.е. состояние всех переменных. Рантаймы можно:
|
||||
* создавать;
|
||||
* проверять статус;
|
||||
* терминировать.
|
||||
|
||||
Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным. Например, `program` будет оперировать данными высшего уровня (рецепт и кофе-машина), дополняя их терминами своего уровня (идентификатор запуска). Это совершенно нормально: API должен связывать контексты.
|
||||
Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным. Например, `program` будет оперировать данными высшего уровня (рецепт и кофемашина), дополняя их терминами своего уровня (идентификатор запуска). Это совершенно нормально: API должен связывать контексты.
|
||||
|
||||
#### Сценарии использования
|
||||
|
||||
@ -43,18 +43,18 @@
|
||||
|
||||
Очевидно, первый шаг — нужно предоставить пользователю возможность выбора, чего он, собственно хочет. И первый же шаг обнажает неудобство использования нашего API: никаких методов, позволяющих пользователю что-то выбрать в нашем API нет. Разработчику придётся сделать что-то типа такого:
|
||||
* получить все доступные рецепты из `GET /v1/recipes`;
|
||||
* получить список всех кофе-машин из `GET /v1/coffee-machines`;
|
||||
* получить список всех кофемашин из `GET /v1/coffee-machines`;
|
||||
* самостоятельно выбрать нужные данные.
|
||||
|
||||
В псевдокоде это будет выглядеть примерно вот так:
|
||||
```
|
||||
// Получить все доступные рецепты
|
||||
let recipes = api.getRecipes();
|
||||
// Получить все доступные кофе-машины
|
||||
// Получить все доступные кофемашины
|
||||
let coffeeMachines = api.getCoffeeMachines();
|
||||
// Построить пространственный индекс
|
||||
let coffeeMachineRecipesIndex = buildGeoIndex(recipes, coffeeMachines);
|
||||
// Выбрать кофе-машины, соответствующие запросу пользователя
|
||||
// Выбрать кофемашины, соответствующие запросу пользователя
|
||||
let matchingCoffeeMachines = coffeeMachineRecipesIndex.query(
|
||||
parameters,
|
||||
{ "sort_by": "distance" }
|
||||
@ -63,7 +63,7 @@ let matchingCoffeeMachines = coffeeMachineRecipesIndex.query(
|
||||
app.display(coffeeMachines);
|
||||
```
|
||||
|
||||
Как видите, разработчику придётся написать немало лишнего кода (это не упоминая о сложности имплементации геопространственных индексов!). Притом, учитывая наши наполеоновские планы по покрытию нашим API всех кофе-машин мира, такой алгоритм выглядит заведомо бессмысленной тратой ресурсов на получение списков и поиск по ним.
|
||||
Как видите, разработчику придётся написать немало лишнего кода (это не упоминая о сложности имплементации геопространственных индексов!). Притом, учитывая наши наполеоновские планы по покрытию нашим API всех кофемашин мира, такой алгоритм выглядит заведомо бессмысленной тратой ресурсов на получение списков и поиск по ним.
|
||||
|
||||
Напрашивается добавление нового эндпойнта поиска. Для того, чтобы разработать этот интерфейс, нам придётся самим встать на место UX-дизайнера и подумать, каким образом приложение будет пытаться заинтересовать пользователя. Два сценария довольно очевидны:
|
||||
* показать ближайшие кофейни и виды предлагаемого кофе в них («service discovery»-сценарий) — для пользователей-новичков, или просто людей без определённых предпочтений;
|
||||
@ -92,9 +92,9 @@ POST /v1/offers/search
|
||||
|
||||
Здесь:
|
||||
* `offer` — некоторое «предложение»: на каких условиях можно заказать запрошенные виды кофе, если они были указаны, либо какое-то маркетинговое предложение — цены на самые популярные / интересные напитки, если пользователь не указал конкретные рецепты для поиска;
|
||||
* `place` — место (кафе, автомат, ресторан), где находится машина; мы не вводили эту сущность ранее, но, очевидно, пользователю потребуются какие-то более понятные ориентиры, нежели географические координаты, чтобы найти нужную кофе-машину.
|
||||
* `place` — место (кафе, автомат, ресторан), где находится машина; мы не вводили эту сущность ранее, но, очевидно, пользователю потребуются какие-то более понятные ориентиры, нежели географические координаты, чтобы найти нужную кофемашину.
|
||||
|
||||
**NB**. Мы могли бы не добавлять новый эндпойнт, а обогатить существующий `/coffee-machines`. Однако такое решение выглядит менее семантично: не стоит в рамках одного интерфейса смешивать способ перечисления объектов по порядку и по релевантности запросу, поскольку эти два вида ранжирования обладают существенно разными свойствами и сценариями использования. К тому же, обогащение поиска «предложениями» скорее выводит эту функциональность из неймспейса «кофе-машины»: для пользователя всё-таки первичен факт получения предложения приготовить напиток на конкретных условиях, и кофе-машина — лишь одно из них. `/v1/offers/search` — более логичное имя для такого эндпойнта.
|
||||
**NB**. Мы могли бы не добавлять новый эндпойнт, а обогатить существующий `/coffee-machines`. Однако такое решение выглядит менее семантично: не стоит в рамках одного интерфейса смешивать способ перечисления объектов по порядку и по релевантности запросу, поскольку эти два вида ранжирования обладают существенно разными свойствами и сценариями использования. К тому же, обогащение поиска «предложениями» скорее выводит эту функциональность из неймспейса «кофемашины»: для пользователя всё-таки первичен факт получения предложения приготовить напиток на конкретных условиях, и кофемашина — лишь одно из них. `/v1/offers/search` — более логичное имя для такого эндпойнта.
|
||||
|
||||
Вернёмся к коду, который напишет разработчик. Теперь он будет выглядеть примерно так:
|
||||
```
|
||||
@ -105,9 +105,9 @@ let offers = api.offerSearch(parameters);
|
||||
app.display(offers);
|
||||
```
|
||||
|
||||
#### Хэлперы
|
||||
#### Хелперы
|
||||
|
||||
Методы, подобные только что изобретённому нами `offers/search`, принято называть *хэлперами*. Цель их существования — обобщить понятные сценарии использования API и облегчить их. Под «облегчить» мы имеем в виду не только сократить многословность («бойлерплейт»), но и помочь разработчику избежать частых проблем и ошибок.
|
||||
Методы, подобные только что изобретённому нами `offers/search`, принято называть *хелперами*. Цель их существования — обобщить понятные сценарии использования API и облегчить их. Под «облегчить» мы имеем в виду не только сократить многословность («бойлерплейт»), но и помочь разработчику избежать частых проблем и ошибок.
|
||||
|
||||
Рассмотрим, например, вопрос стоимости заказа. Наша функция поиска возвращает какие-то «предложения» с ценой. Но ведь цена может меняться: в «счастливый час» кофе может стоить меньше. Разработчик может ошибиться в имплементации этой функциональности трижды:
|
||||
* кэшировать на клиентском устройстве результаты поиска слишком долго (в результате цена всегда будет неактуальна),
|
||||
@ -199,15 +199,15 @@ POST /v1/orders
|
||||
|
||||
Бороться с этим законом можно только одним способом: декомпозицией. На каждом уровне работы с вашим API нужно стремиться логически группировать сущности под одним именем там, где это возможно и таким образом, чтобы разработчику никогда не приходилось оперировать более чем 10 сущностями одновременно.
|
||||
|
||||
Рассмотрим простой пример: что должна возвращать функция поиска подходящей кофе-машины. Для обеспечения хорошего UX приложения необходимо передать довольно значительные объёмы информации.
|
||||
Рассмотрим простой пример: что должна возвращать функция поиска подходящей кофемашины. Для обеспечения хорошего UX приложения необходимо передать довольно значительные объёмы информации.
|
||||
```
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"coffee_machine_id",
|
||||
// Тип кофе-машины
|
||||
// Тип кофемашины
|
||||
"coffee_machine_type": "drip_coffee_maker",
|
||||
// Марка кофе-машины
|
||||
// Марка кофемашины
|
||||
"coffee_machine_brand",
|
||||
// Название заведения
|
||||
"place_name": "Кафе «Ромашка»",
|
||||
@ -221,7 +221,7 @@ POST /v1/orders
|
||||
// Сколько идти: время и расстояние
|
||||
"walking_distance",
|
||||
"walking_time",
|
||||
// Как найти заведение и кофе-машину
|
||||
// Как найти заведение и кофемашину
|
||||
"place_location_tip",
|
||||
"offers": [
|
||||
{
|
||||
@ -247,8 +247,8 @@ POST /v1/orders
|
||||
Подход, увы, совершенно стандартный, его можно встретить практически в любом API. Как мы видим, количество полей сущностей вышло далеко за рекомендованные 7, и даже 9. При этом набор полей идёт плоским списком вперемешку, часто с одинаковыми префиксами.
|
||||
|
||||
В такой ситуации мы должны выделить в структуре информационные домены: какие поля логически относятся к одной предметной области. В данном случае мы можем выделить как минимум следующие виды данных:
|
||||
* данные о заведении, в котором находится кофе машины;
|
||||
* данные о самой кофе-машине;
|
||||
* данные о заведении, в котором находится кофемашины;
|
||||
* данные о самой кофемашине;
|
||||
* данные о пути до точки;
|
||||
* данные о рецепте;
|
||||
* особенности рецепта в конкретном заведении;
|
||||
@ -261,7 +261,7 @@ POST /v1/orders
|
||||
"results": [{
|
||||
// Данные о заведении
|
||||
"place": { "name", "location" },
|
||||
// Данные о кофе-машине
|
||||
// Данные о кофемашине
|
||||
"coffee-machine": { "id", "brand", "type" },
|
||||
// Как добраться
|
||||
"route": { "distance", "duration", "location_tip" },
|
||||
@ -270,7 +270,7 @@ POST /v1/orders
|
||||
// Рецепт
|
||||
"recipe": { "id", "name", "description" },
|
||||
// Данные относительно того,
|
||||
// как рецепт готовят на конкретной кофе-машине
|
||||
// как рецепт готовят на конкретной кофемашине
|
||||
"options": { "volume" },
|
||||
// Метаданные предложения
|
||||
"offer": { "id", "valid_until" },
|
||||
|
@ -131,7 +131,7 @@ strpbrk (str1, str2)
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
// Возвращает список встроенных функций кофе-машины
|
||||
// Возвращает список встроенных функций кофемашины
|
||||
GET /coffee-machines/{id}/functions
|
||||
```
|
||||
Слово "functions" многозначное: оно может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).
|
||||
@ -1021,7 +1021,7 @@ POST /search
|
||||
|
||||
Следует иметь в виду, что явной передачи локации может оказаться недостаточно, поскольку в мире существуют территориальные конфликты и спорные территории. Каким образом API должен себя вести при попадании координат пользователя на такие территории — вопрос, к сожалению, в первую очередь юридический. Автору этой книги приходилось как-то разрабатывать API, в котором пришлось вводить концепцию «территория государства A по мнению официальных органов государства Б».
|
||||
|
||||
**Важно**: различайте локализацию для конечного пользователя и локализацию для разработчика. В примере из п. 19 сообщение `localized_message` адресовано пользователю — его должно показать приложение, если в коде обработка такой ошибки не предусмотрена. Это сообщение должно быть написано на указанном в запросе языке и отформатировано согласно правилам локации пользователя. А вот сообщение `details.checks_failed[].message` написано не для пользователя, а для разработчика, который будет разбираться с проблемой. Соответственно, написано и отформатировано оно должно быть понятным для разработчика образом — что, скорее всего, означает «на английском языке», т.к. английский де факто является стандартом в мире разработки программного обеспечения.
|
||||
**Важно**: различайте локализацию для конечного пользователя и локализацию для разработчика. В примере из п. 19 сообщение `localized_message` адресовано пользователю — его должно показать приложение, если в коде обработка такой ошибки не предусмотрена. Это сообщение должно быть написано на указанном в запросе языке и отформатировано согласно правилам локации пользователя. А вот сообщение `details.checks_failed[].message` написано не для пользователя, а для разработчика, который будет разбираться с проблемой. Соответственно, написано и отформатировано оно должно быть понятным для разработчика образом — что, скорее всего, означает «на английском языке», т.к. английский де-факто является стандартом в мире разработки программного обеспечения.
|
||||
|
||||
Следует отметить, что индикация, какие сообщения следует показать пользователю, а какие написаны для разработчика, должна, разумеется, быть явной конвенцией вашего API. В примере для этого используется префикс `localized_`.
|
||||
|
||||
|
@ -19,7 +19,7 @@ POST /v1/offers/search
|
||||
"results": [{
|
||||
// Данные о заведении
|
||||
"place": { "name", "location" },
|
||||
// Данные о кофе-машине
|
||||
// Данные о кофемашине
|
||||
"coffee_machine": { "id", "brand", "type" },
|
||||
// Как добраться
|
||||
"route": { "distance", "duration", "location_tip" },
|
||||
@ -28,7 +28,7 @@ POST /v1/offers/search
|
||||
// Рецепт
|
||||
"recipe": { "id", "name", "description" },
|
||||
// Данные относительно того,
|
||||
// как рецепт готовят на конкретной кофе-машине
|
||||
// как рецепт готовят на конкретной кофемашине
|
||||
"options": { "volume" },
|
||||
// Метаданные предложения
|
||||
"offer": { "id", "valid_until" },
|
||||
@ -88,7 +88,7 @@ POST /v1/orders/{id}/cancel
|
||||
```
|
||||
// Возвращает идентификатор программы,
|
||||
// соответствующей указанному рецепту
|
||||
// на указанной кофе-машине
|
||||
// на указанной кофемашине
|
||||
POST /v1/program-matcher
|
||||
{ "recipe", "coffee_machine" }
|
||||
→
|
||||
|
@ -60,7 +60,7 @@
|
||||
* старая функциональность перестаёт поддерживаться;
|
||||
* меняются интерфейсы.
|
||||
|
||||
Как правило, API изначально покрывает только какую-то часть существующей предметной области. В случае нашего [примера с API кофе-машин](https://twirl.github.io/The-API-Book/docs/API.ru.html#chapter-7) разумно ожидать, что будут появляться новые модели с новым API, которые нам придётся включать в свою платформу, и гарантировать возможность сохранения того же интерфейса абстракции — весьма непросто. Даже если просто добавлять поддержку новых видов нижележащих устройств, не добавляя ничего во внешний интерфейс — это всё равно изменения в коде, которые могут в итоге привести к несовместимости, пусть и ненамеренно.
|
||||
Как правило, API изначально покрывает только какую-то часть существующей предметной области. В случае нашего [примера с API кофемашин](https://twirl.github.io/The-API-Book/docs/API.ru.html#chapter-7) разумно ожидать, что будут появляться новые модели с новым API, которые нам придётся включать в свою платформу, и гарантировать возможность сохранения того же интерфейса абстракции — весьма непросто. Даже если просто добавлять поддержку новых видов нижележащих устройств, не добавляя ничего во внешний интерфейс — это всё равно изменения в коде, которые могут в итоге привести к несовместимости, пусть и ненамеренно.
|
||||
|
||||
Стоит также отметить, что далеко не все поставщики API относятся к поддержанию обратной совместимости, да и вообще к качеству своего ПО, так же серьёзно, как и (надеемся) вы. Стоит быть готовым к тому, что заниматься поддержанием вашего API в рабочем состоянии, то есть написанием и поддержкой фасадов к меняющемуся ландшафту предметной области, придётся именно вам, и зачастую довольно внезапно.
|
||||
|
||||
@ -83,7 +83,7 @@
|
||||
|
||||
1. Как часто выпускать мажорные версии API.
|
||||
|
||||
Это в основном *продуктовый* вопрос. Новая мажорная версия API выпускается, когда накоплена критическая масса функциональности, которую невозможно или слишком дорого поддерживать в рамках предыдущей мажорной версии. В стабильной ситуации такая необходимость возникает, как правило, раз в несколько лет. На динамично развивающихся рынках новые мажорные версии можно выпускать чаще, здесь ограничителем являются только ваши возможности выделить достаточно ресурсов для поддержания зоопарка версий. Однако следует заметить, что выпуск новой мажорной версии раньше, чем была стабилизирована предыдущая (а на это как правило требуется от нескольких месяцев до года), выглядит для разработчиков очень плохим сигналом, означающим риск *постоянно* сидеть на сырой платформе.
|
||||
Это в основном *продуктовый* вопрос. Новая мажорная версия API выпускается, когда накоплена критическая масса функциональности, которую невозможно или слишком дорого поддерживать в рамках предыдущей мажорной версии. В стабильной ситуации такая необходимость возникает, как правило, раз в несколько лет. На динамично развивающихся рынках новые мажорные версии можно выпускать чаще, здесь ограничителем являются только ваши возможности выделить достаточно ресурсов для поддержания зоопарка версий. Однако следует заметить, что выпуск новой мажорной версии раньше, чем была стабилизирована предыдущая (а на это, как правило, требуется от нескольких месяцев до года), выглядит для разработчиков очень плохим сигналом, означающим риск *постоянно* сидеть на сырой платформе.
|
||||
|
||||
2. Какое количество *мажорных* версий поддерживать одновременно.
|
||||
|
||||
|
@ -8,9 +8,9 @@
|
||||
|
||||
Начнём с базового интерфейса. Предположим, что мы пока что вообще не раскрывали никакой функциональности помимо поиска предложений и заказа, т.е. мы предоставляем API из двух методов — `POST /offers/search` и `POST /orders`.
|
||||
|
||||
Сделаем следующий логический шаг и предположим, что партнёры захотят динамически подключать к нашей платформе свои собственные кофе машины с каким-то новым API. Для этого нам будет необходимо договориться о формате обратного вызова, каким образом мы будем вызывать API партнёра, и предоставить два новых эндпойта для:
|
||||
Сделаем следующий логический шаг и предположим, что партнёры захотят динамически подключать к нашей платформе свои собственные кофемашины с каким-то новым API. Для этого нам будет необходимо договориться о формате обратного вызова, каким образом мы будем вызывать API партнёра, и предоставить два новых эндпойнта для:
|
||||
* регистрации в системе новых типов API;
|
||||
* загрузки списка кофе-машин партнёра с указанием типа API.
|
||||
* загрузки списка кофемашин партнёра с указанием типа API.
|
||||
|
||||
Например, можно предоставить вот такие методы.
|
||||
|
||||
@ -25,7 +25,7 @@ PUT /v1/api-types/{api_type}
|
||||
```
|
||||
|
||||
```
|
||||
// 2. Предоставить список кофе-машин с разбивкой
|
||||
// 2. Предоставить список кофемашин с разбивкой
|
||||
// по типу API
|
||||
PUT /v1/partners/{partnerId}/coffee-machines
|
||||
{
|
||||
@ -39,19 +39,19 @@ PUT /v1/partners/{partnerId}/coffee-machines
|
||||
```
|
||||
|
||||
Таким образом механика следующая:
|
||||
* партнёр описывает свои виды API, кофе-машины и поддерживаемые рецепты;
|
||||
* при получении заказа, который необходимо выполнить на конкретной кофе машине, наш сервер обратится к функции обратного вызова, передав ей данные о заказе в оговорённом формате.
|
||||
* партнёр описывает свои виды API, кофемашины и поддерживаемые рецепты;
|
||||
* при получении заказа, который необходимо выполнить на конкретной кофемашине, наш сервер обратится к функции обратного вызова, передав ей данные о заказе в оговорённом формате.
|
||||
|
||||
Теперь партнёры могут динамически подключать свои кофе-машины и обрабатывать заказы. Займёмся теперь, однако, вот каким упражнением:
|
||||
Теперь партнёры могут динамически подключать свои кофемашины и обрабатывать заказы. Займёмся теперь, однако, вот каким упражнением:
|
||||
* перечислим все неявные предположения, которые мы допустили;
|
||||
* перечислим все неявные механизмы связывания, которые необходимы для функционирования платформы.
|
||||
|
||||
Может показаться, что в нашем API нет ни того, ни другого, ведь он очень прост и по сути просто сводится к вызову какого-то HTTP-метода — но это неправда.
|
||||
1. Предполагается, что каждая кофе-машина поддерживает все возможные опции заказа (например, допустимый объём напитка).
|
||||
2. Нет необходимости показывать пользователю какую-то дополнительную информацию о том, что заказ готовится на новых типах кофе-машин.
|
||||
3. Цена напитка не зависит ни от партнёра, ни от типа кофе-машины.
|
||||
1. Предполагается, что каждая кофемашина поддерживает все возможные опции заказа (например, допустимый объём напитка).
|
||||
2. Нет необходимости показывать пользователю какую-то дополнительную информацию о том, что заказ готовится на новых типах кофемашин.
|
||||
3. Цена напитка не зависит ни от партнёра, ни от типа кофемашины.
|
||||
|
||||
Эти пункты мы выписали с одной целью: нам нужно понять, каким конкретно образом мы будем переводить неявные договорённости в явные, если нам это потребуется. Например, если разные кофе-машины предоставляют разный объём функциональности — допустим, в каких-то кофейнях объём кофе фиксирован — что должно измениться в нашем API?
|
||||
Эти пункты мы выписали с одной целью: нам нужно понять, каким конкретно образом мы будем переводить неявные договорённости в явные, если нам это потребуется. Например, если разные кофемашины предоставляют разный объём функциональности — допустим, в каких-то кофейнях объём кофе фиксирован — что должно измениться в нашем API?
|
||||
|
||||
Универсальный паттерн внесения подобных изменений таков: мы должны рассмотреть существующий интерфейс как частный случай некоторого более общего, в котором значения некоторых параметров приняты известными по умолчанию, а потому опущены. Таким образом, внесение изменений всегда происходит в три шага.
|
||||
1. Явная фиксация программного контракта *в том объёме, в котором она действует на текущий момент*.
|
||||
@ -60,7 +60,7 @@ PUT /v1/partners/{partnerId}/coffee-machines
|
||||
|
||||
На нашем примере с изменением списка доступных опций заказа мы должны поступить следующим образом.
|
||||
|
||||
1. Документируем текущее состояние. Все кофе-машины, подключаемые по API, обязаны поддерживать три опции: посыпку корицей, изменение объёма и бесконтактную выдачу.
|
||||
1. Документируем текущее состояние. Все кофемашины, подключаемые по API, обязаны поддерживать три опции: посыпку корицей, изменение объёма и бесконтактную выдачу.
|
||||
|
||||
2. Добавляем новый метод `with-options`:
|
||||
```
|
||||
@ -86,7 +86,7 @@ PUT /v1/partners/{partnerId}/coffee-machines
|
||||
|
||||
#### Границы применимости
|
||||
|
||||
Хотя это упражнение выглядит весьма простым и универсальным, его использование возможно только при наличии хорошо продуманной архитектуры сущностей и, что ещё более важного, понятного вектора дальнейшего развития API. Представим, что через какое-то время к поддерживаемым опциям добавились новые — ну, скажем, добавление сиропа и второго шота эспрессо. Список опций расширить мы можем — а вот изменить соглашение по умолчанию уже нет. Через некоторое время это приведёт к тому, что «дефолтный» интерфейс `PUT /coffee-machines` окажется никому не нужен, поскольку «дефолтный» список из трёх опций окажется не только редко востребованным, но и просто абсурдным — почему эти три, чем они лучше всех остальных? По сути значения по умолчанию и номенклатура старых методов начнут отражать исторические этапы развития нашего API, а это совершенно не то, чего мы хотели бы от номенклатуры хелперов и значений по умолчанию.
|
||||
Хотя это упражнение выглядит весьма простым и универсальным, его использование возможно только при наличии хорошо продуманной архитектуры сущностей и, что ещё более важно, понятного вектора дальнейшего развития API. Представим, что через какое-то время к поддерживаемым опциям добавились новые — ну, скажем, добавление сиропа и второго шота эспрессо. Список опций расширить мы можем — а вот изменить соглашение по умолчанию уже нет. Через некоторое время это приведёт к тому, что «дефолтный» интерфейс `PUT /coffee-machines` окажется никому не нужен, поскольку «дефолтный» список из трёх опций окажется не только редко востребованным, но и просто абсурдным — почему эти три, чем они лучше всех остальных? По сути значения по умолчанию и номенклатура старых методов начнут отражать исторические этапы развития нашего API, а это совершенно не то, чего мы хотели бы от номенклатуры хелперов и значений по умолчанию.
|
||||
|
||||
Увы, здесь мы сталкиваемся с плохо разрешимым противоречием: мы хотим, с одной стороны, чтобы разработчик писал лаконичный код, следовательно, должны предоставлять хорошие хелперные методы и значения по умолчанию. С другой, знать наперёд какими будут самые частотные наборы опций через несколько лет развития API — очень сложно.
|
||||
|
||||
|
@ -97,7 +97,7 @@ GET /v1/layouts/{layout_id}
|
||||
"id",
|
||||
// Макетов вполне возможно будет много разных,
|
||||
// поэтому имеет смысл сразу заложить
|
||||
// расширяемоесть
|
||||
// расширяемость
|
||||
"kind": "recipe_search",
|
||||
// Описываем каждое свойство рецепта,
|
||||
// которое должно быть задано для
|
||||
|
@ -12,11 +12,11 @@ GET /v1/recipes/{id}/run-data/{api_type}
|
||||
```
|
||||
|
||||
Тогда разработчикам пришлось бы сделать примерно следующее для запуска приготовления кофе:
|
||||
* выяснить тип API конкретной кофе-машины;
|
||||
* выяснить тип API конкретной кофемашины;
|
||||
* получить описание способа запуска программы выполнения рецепта на машине с API такого типа;
|
||||
* в зависимости от типа API выполнить специфические команды запуска.
|
||||
|
||||
Очевидно, что такой интерфейс совершенно недопустим — просто потому, что в подавляющем большинстве случаев разработчикам совершенно неинтересно, какого рода API поддерживает та или иная кофе-машина. Для того, чтобы не допустить такого плохого интерфейса мы ввели новую сущность «программа», которая по факту представляет собой не более чем просто идентификатор контекста, как и сущность «рецепт».
|
||||
Очевидно, что такой интерфейс совершенно недопустим — просто потому, что в подавляющем большинстве случаев разработчикам совершенно неинтересно, какого рода API поддерживает та или иная кофемашина. Для того чтобы не допустить такого плохого интерфейса, мы ввели новую сущность «программа», которая по факту представляет собой не более чем просто идентификатор контекста, как и сущность «рецепт».
|
||||
|
||||
Аналогичным образом устроена и сущность `program_run_id`, идентификатор запуска программы. Он также по сути не имеет почти никакого интерфейса и состоит только из идентификатора запуска.
|
||||
|
||||
@ -35,7 +35,7 @@ PUT /v1/api-types/{api_type}
|
||||
|
||||
```
|
||||
// Эндпойнт добавления списка
|
||||
// кофе-машин партнёра
|
||||
// кофемашин партнёра
|
||||
PUT /v1/api-types/{api_type}
|
||||
{
|
||||
"order_execution_endpoint":
|
||||
@ -87,7 +87,7 @@ PUT /v1/api-types/{api_type}
|
||||
|
||||
```
|
||||
/* Имплементация партнёром интерфейса
|
||||
запуска программы на его кофе-машинах */
|
||||
запуска программы на его кофемашинах */
|
||||
registerProgramRunHandler(apiType, (context) => {
|
||||
// Инициализируем запуск исполнения
|
||||
// программы на стороне партнёра
|
||||
@ -133,7 +133,7 @@ registerProgramRunHandler(apiType, (context) => {
|
||||
|
||||
```
|
||||
/* Имплементация партнёром интерфейса
|
||||
запуска программы на его кофе-машинах */
|
||||
запуска программы на его кофемашинах */
|
||||
registerProgramRunHandler(apiType, (context) => {
|
||||
// Инициализируем запуск исполнения
|
||||
// программы на стороне партнёра
|
||||
@ -210,9 +210,9 @@ ProgramContext.dispatch = (action) => {
|
||||
|
||||
Описав указанным выше образом взаимодействие со сторонними API, мы можем (и должны) теперь рассмотреть вопрос, совместимы ли эти интерфейсы с нашими собственными абстракциями, которые мы разработали в [главе 9](#chapter-9); иными словами, можно ли запустить исполнение такого заказа, оперируя не высокоуровневым, а низкоуровневым API.
|
||||
|
||||
Напомним, что мы предложили вот такие абстрактные интерфейсы для работы с произвольными типами API кофе-машин:
|
||||
Напомним, что мы предложили вот такие абстрактные интерфейсы для работы с произвольными типами API кофемашин:
|
||||
|
||||
* `POST /v1/program-matcher` возвращает идентификатор программы по идентификатору кофе-машины и рецепта;
|
||||
* `POST /v1/program-matcher` возвращает идентификатор программы по идентификатору кофемашины и рецепта;
|
||||
* `POST /v1/programs/{id}/run` запускает программу на исполнение.
|
||||
|
||||
Как легко убедиться, добиться совместимости с этими интерфейсами очень просто: для этого достаточно присвоить идентификатор `program_id` паре (тип API, рецепт), например, вернув его из метода `PUT /coffee-machines`:
|
||||
@ -245,7 +245,7 @@ PUT /v1/partners/{partnerId}/coffee-machines
|
||||
POST /v1/programs/{id}/run
|
||||
```
|
||||
|
||||
будет работать и с партнёрскими кофе-машинами (читай, с третьим видом API).
|
||||
будет работать и с партнёрскими кофемашинами (читай, с третьим видом API).
|
||||
|
||||
#### Делегируй!
|
||||
|
||||
@ -253,4 +253,4 @@ POST /v1/programs/{id}/run
|
||||
|
||||
Напротив, применяя парадигму конкретизации контекста на каждом новом уровне абстракции мы рано или поздно спустимся вниз по кроличьей норе достаточно глубоко, чтобы конкретизировать было уже нечего: контекст однозначно соотносится с функциональностью, доступной для программного управления. И вот на этом уровне мы должны отказаться от дальнейшей детализации и непосредственно реализовать нужные алгоритмы. Важно отметить, что глубина абстрагирования будет различной для различных нижележащих платформ.
|
||||
|
||||
**NB**. В рамках [главы 9](#chapter-9) мы именно этот принцип и проиллюстрировали: в рамках API кофе-машин первого типа нет нужды продолжать растить дерево абстракций, можно ограничиться запуском программ; в рамках API второго типа требуется дополнительный промежуточный контекст в виде рантаймов.
|
||||
**NB**. В рамках [главы 9](#chapter-9) мы именно этот принцип и проиллюстрировали: в рамках API кофемашин первого типа нет нужды продолжать растить дерево абстракций, можно ограничиться запуском программ; в рамках API второго типа требуется дополнительный промежуточный контекст в виде рантаймов.
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
**NB**. В этих правилах нет ничего особенно нового: в них легко опознаются принципы архитектуры [SOLID](https://en.wikipedia.org/wiki/SOLID) — что неудивительно, поскольку SOLID концентрируется на контрактно-ориентированном подходе к разработке, а API по определению и есть контракт. Мы лишь добавляем в эти принципы понятие уровней абстракции и информационных контекстов.
|
||||
|
||||
Остаётся, однако, неотвеченным вопрос о том, как изначально выстроить номенклатуру сущностей таким образом, чтобы расширение API не превращало её в мешанину из различных неконсистентных методов разных эпох. Впрочем, ответ на него довольно очевиден: чтобы при абстрагировании не возникало неловких ситуаций, подобно рассмотренному нами примеру с поддерживаемыми кофе-машиной опциями, все сущности необходимо *изначально* рассматривать как частную реализацию некоторого более общего интерфейса, даже если никаких альтернативных реализаций в настоящий момент не предвидится.
|
||||
Остаётся, однако, неотвеченным вопрос о том, как изначально выстроить номенклатуру сущностей таким образом, чтобы расширение API не превращало её в мешанину из различных неконсистентных методов разных эпох. Впрочем, ответ на него довольно очевиден: чтобы при абстрагировании не возникало неловких ситуаций, подобно рассмотренному нами примеру с поддерживаемыми кофемашиной опциями, все сущности необходимо *изначально* рассматривать как частную реализацию некоторого более общего интерфейса, даже если никаких альтернативных реализаций в настоящий момент не предвидится.
|
||||
|
||||
Например, разрабатывая API эндпойнта `POST /search` мы должны были задать себе вопрос: а «результат поиска» — это абстракция над каким интерфейсом? Для этого нам нужно аккуратно декомпозировать эту сущность, чтобы понять, каким своим срезом она выступает во взаимодействии с каким объектами.
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
##### Изолируйте зависимости
|
||||
|
||||
В случае, если API является гейтвеем, предоставляющим доступ к какому-то нижележащему API или агрегирующим несколько различных API за одним фасадом, велик соблазн предоставить оригинальный интерфейс as is, не внося в него изменений и не усложняя себя жизнь разработкой слабо связанного взаимодействия. Например, разрабатывая интерфейс для запуска программ, описанный в [главе 9](#chapter-9), мы могли бы взять за основу интерфейс кофе-машин первого типа и предоставить его в виде API, проксируя запросы и ответы как есть. Делать так ни в коем случае нельзя по нескольким причинам:
|
||||
В случае, если API является гейтвеем, предоставляющим доступ к какому-то нижележащему API или агрегирующим несколько различных API за одним фасадом, велик соблазн предоставить оригинальный интерфейс as is, не внося в него изменений и не усложняя себя жизнь разработкой слабо связанного взаимодействия. Например, разрабатывая интерфейс для запуска программ, описанный в [главе 9](#chapter-9), мы могли бы взять за основу интерфейс кофемашин первого типа и предоставить его в виде API, проксируя запросы и ответы как есть. Делать так ни в коем случае нельзя по нескольким причинам:
|
||||
* как правило, у вас нет никаких гарантий, что партнёр будет поддерживать свой API в обратно-совместимом или хотя бы концептуально похожем виде;
|
||||
* любые проблемы партнёра будут автоматически отражаться на ваших клиентах.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user