mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-01-05 10:20:22 +02:00
Merge pull request #29 from Cliffart44/spelling-improvements
Visual & spelling improvements
This commit is contained in:
commit
27c244deb4
@ -10,7 +10,7 @@
|
||||
```
|
||||
// Описание метода
|
||||
POST /v1/bucket/{id}/some-resource
|
||||
X-Idempotency-Token: <токен идемпотентости>
|
||||
X-Idempotency-Token: <токен идемпотентности>
|
||||
{
|
||||
…
|
||||
// Это однострочный комментарий
|
||||
|
@ -6,7 +6,7 @@
|
||||
* разграничение областей ответственности;
|
||||
* описание конечных интерфейсов.
|
||||
|
||||
Этот алгоритм строит API сверху вниз, от общих требований и сценариев использования до конкретной номенклатуры сущностей; фактически, двигаясь этим путем, вы получите на выходе готовый API — чем этот подход и ценен.
|
||||
Этот алгоритм строит API сверху вниз, от общих требований и сценариев использования до конкретной номенклатуры сущностей; фактически, двигаясь этим путём, вы получите на выходе готовый API — чем этот подход и ценен.
|
||||
|
||||
Может показаться, что наиболее полезные советы приведены в последнем разделе, однако это не так; цена ошибки, допущенной на разных уровнях весьма различна. Если исправить плохое именование довольно просто, то исправить неверное понимание того, зачем вообще нужен API, практически невозможно.
|
||||
|
||||
|
@ -12,11 +12,11 @@
|
||||
|
||||
Итак, предположим, что мы хотим предоставить API автоматического заказа кофе в городских кофейнях. Попробуем применить к нему этот принцип.
|
||||
|
||||
1. Зачем кому-то может потребоваться API для приготовления кофе? В чем неудобство заказа кофе через интерфейс, человек-человек или человек-машина? Зачем нужна возможность заказа машина-машина?
|
||||
1. Зачем кому-то может потребоваться API для приготовления кофе? В чём неудобство заказа кофе через интерфейс, человек-человек или человек-машина? Зачем нужна возможность заказа машина-машина?
|
||||
|
||||
* Возможно, мы хотим решить проблему выбора и знания? Чтобы человек наиболее полно знал о доступных ему здесь и сейчас опциях.
|
||||
* Возможно, мы оптимизируем время ожидания? Чтобы человеку не пришлось ждать, пока его заказ готовится.
|
||||
* Возможно, мы хотим минимизировать ошибки? Чтобы человек получил именно то, что хотел заказать, не потеряв информацию при разговорном общении либо при настройке незнакомого интерфейса кофе-машины.
|
||||
* Возможно, мы хотим минимизировать ошибки? Чтобы человек получил именно то, что хотел заказать, не потеряв информацию при разговорном общении либо при настройке незнакомого интерфейса кофемашины.
|
||||
|
||||
Вопрос «зачем» — самый важный из тех вопросов, которые вы должны задавать себе. Не только глобально в отношении целей всего проекта, но и локально в отношении каждого кусочка функциональности. **Если вы не можете коротко и понятно ответить на вопрос «зачем эта сущность нужна» — значит, она не нужна**.
|
||||
|
||||
@ -24,17 +24,17 @@
|
||||
|
||||
2. Правда ли решаемая проблема существует? Действительно ли мы наблюдаем неравномерную загрузку кофейных автоматов по утрам? Правда ли люди страдают от того, что не могут найти поблизости нужный им латте с ореховым сиропом? Действительно ли людям важны те минуты, которые они теряют, стоя в очередях?
|
||||
|
||||
3. Действительно ли мы обладаем достаточным ресурсом, чтобы решить эту проблему? Есть ли у нас доступ к достаточному количеству кофе-машин и клиентов, чтобы обеспечить работоспособность системы?
|
||||
3. Действительно ли мы обладаем достаточным ресурсом, чтобы решить эту проблему? Есть ли у нас доступ к достаточному количеству кофемашин и клиентов, чтобы обеспечить работоспособность системы?
|
||||
|
||||
4. Наконец, правда ли мы решим проблему? Как мы поймём, что оптимизировали перечисленные факторы?
|
||||
|
||||
На все эти вопросы, в общем случае, простого ответа нет. В идеале ответы на эти вопросы должны даваться с цифрами в руках. Сколько конкретно времени тратится неоптимально, и какого значения мы рассчитываем добиться, располагая какой плотностью кофе-машин? Заметим также, что в реальной жизни просчитать такого рода цифры можно в основном для проектов, которые пытаются влезть на уже устоявшийся рынок; если вы пытаетесь сделать что-то новое, то, вероятно, вам придётся ориентироваться в основном на свою интуицию.
|
||||
На все эти вопросы, в общем случае, простого ответа нет. В идеале ответы на эти вопросы должны даваться с цифрами в руках. Сколько конкретно времени тратится неоптимально, и какого значения мы рассчитываем добиться, располагая какой плотностью кофемашин? Заметим также, что в реальной жизни просчитать такого рода цифры можно в основном для проектов, которые пытаются влезть на уже устоявшийся рынок; если вы пытаетесь сделать что-то новое, то, вероятно, вам придётся ориентироваться в основном на свою интуицию.
|
||||
|
||||
#### Почему API?
|
||||
|
||||
Поскольку наша книга посвящена не просто разработке программного обеспечения, а разработке API, то на все эти вопросы мы должны взглянуть под другим ракурсом: а почему для решения этих задач требуется именно API, а не просто программное обеспечение? В нашем вымышленном примере мы должны спросить себя: зачем нам нужно предоставлять сервис для других разработчиков, чтобы они могли готовить кофе своим клиентам, а не сделать своё приложение для конечного потребителя?
|
||||
|
||||
Иными словами, должна иметься веская причина, по которой два домена разработки ПО должны быть разделены: есть оператор(ы), предоставляющий API; есть оператор(ы), предоставляющий сервисы пользователям. Их интересы в чем-то различны настолько, что объединение этих двух ролей в одном лице нежелательно. Более подробно мы изложим причины и мотивации делать именно API в разделе III.
|
||||
Иными словами, должна иметься веская причина, по которой два домена разработки ПО должны быть разделены: есть оператор(ы), предоставляющий API; есть оператор(ы), предоставляющий сервисы пользователям. Их интересы в чём-то различны настолько, что объединение этих двух ролей в одном лице нежелательно. Более подробно мы изложим причины и мотивации делать именно API в разделе III.
|
||||
|
||||
Заметим также следующее: вы должны браться делать API тогда и только тогда, когда в ответе на второй вопрос написали «потому что в этом состоит наша экспертиза». Разрабатывая API, вы занимаетесь некоторой мета-разработкой: вы пишете ПО для того, чтобы другие могли разрабатывать ПО для решения задачи пользователя. Не обладая экспертизой в обоих этих доменах (API и конечные продукты) написать хороший API сложно.
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
### Разделение уровней абстракции
|
||||
|
||||
«Разделите свой код на уровни абстракции» - пожалуй, самый общий совет для разработчиков программного обеспечения. Однако будет вовсе не преувеличением сказать, что изоляция уровней абстракции — самая сложная задача, стоящая перед разработчиком API.
|
||||
«Разделите свой код на уровни абстракции» — пожалуй, самый общий совет для разработчиков программного обеспечения. Однако будет вовсе не преувеличением сказать, что изоляция уровней абстракции — самая сложная задача, стоящая перед разработчиком API.
|
||||
|
||||
Прежде чем переходить к теории, следует чётко сформулировать, _зачем_ нужны уровни абстракции и каких целей мы хотим достичь их выделением.
|
||||
|
||||
Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим?
|
||||
Вспомним, что программный продукт — это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят — тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим?
|
||||
|
||||
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; низкоуровневый - отражать декомпозицию сценариев на составные части.
|
||||
1. От сценариев использования к их внутренней реализации: высокоуровневые сущности и номенклатура их методов должны напрямую отражать сценарии использования API; низкоуровневый — отражать декомпозицию сценариев на составные части.
|
||||
|
||||
2. От терминов предметной области пользователя к терминам предметной области исходных данных — в нашем случае от высокоуровневых понятий «рецепт», «заказ», «кофейня» к низкоуровневым «температура напитка» и «координаты кофе-машины»
|
||||
2. От терминов предметной области пользователя к терминам предметной области исходных данных — в нашем случае от высокоуровневых понятий «рецепт», «заказ», «кофейня» к низкоуровневым «температура напитка» и «координаты кофемашины»
|
||||
|
||||
3. Наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к «сырым» - в нашем случае от «лунго» и «сети кофеен "Ромашка"» - к сырым байтовый данным, описывающим состояние кофе-машины марки «Доброе утро» в процессе приготовления напитка.
|
||||
3. Наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к «сырым» — в нашем случае от «лунго» и «сети кофеен "Ромашка"» — к сырым байтовый данным, описывающим состояние кофемашины марки «Доброе утро» в процессе приготовления напитка.
|
||||
|
||||
Чем дальше находятся друг от друга программные контексты, которые соединяет наш API - тем более глубокая иерархия сущностей должна получиться у нас в итоге.
|
||||
Чем дальше находятся друг от друга программные контексты, которые соединяет наш 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 и облегчить их. Под «облегчить» мы имеем в виду не только сократить многословность («бойлерплейт»), но и помочь разработчику избежать частых проблем и ошибок.
|
||||
|
||||
Рассмотрим, например, вопрос стоимости заказа. Наша функция поиска возвращает какие-то «предложения» с ценой. Но ведь цена может меняться: в «счастливый час» кофе может стоить меньше. Разработчик может ошибиться в имплементации этой функциональности трижды:
|
||||
* кэшировать на клиентском устройстве результаты поиска слишком долго (в результате цена всегда будет неактуальна),
|
||||
@ -189,7 +189,7 @@ POST /v1/orders
|
||||
|
||||
Получив такую ошибку, клиент должен проверить её род (что-то с предложением), проверить конкретную причину ошибки (срок жизни оффера истёк) и отправить повторный запрос цены. При этом если бы `checks_failed` показал другую причину ошибки — например, указанный `offer_id` не принадлежит данному пользователю — действия клиента были бы иными (отправить пользователя повторно авторизоваться, а затем перезапросить цену). Если же обработка такого рода ошибок в коде не предусмотрена — следует показать пользователю сообщение `localized_message` и вернуться к обработке ошибок по умолчанию.
|
||||
|
||||
Важно также отметить, что неустранимые ошибки в моменте для клиента бесполезны (не зная причины ошибки клиент не может ничего разумного предложить пользователю), но это не значит, что у них не должно быть расширенной информации: их все равно будет просматривать разработчик, когда будет исправлять эту проблему в коде. Подробнее об этом в пп. 12-13 следующей главы.
|
||||
Важно также отметить, что неустранимые ошибки в моменте для клиента бесполезны (не зная причины ошибки клиент не может ничего разумного предложить пользователю), но это не значит, что у них не должно быть расширенной информации: их всё равно будет просматривать разработчик, когда будет исправлять эту проблему в коде. Подробнее об этом в пп. 12-13 следующей главы.
|
||||
|
||||
#### Декомпозиция интерфейсов. Правило «7±2»
|
||||
|
||||
@ -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" },
|
||||
@ -285,4 +285,4 @@ POST /v1/orders
|
||||
|
||||
Важно, что читабельность достигается не просто снижением количества сущностей на одном уровне. Декомпозиция должна производиться таким образом, чтобы разработчик при чтении интерфейса сразу понимал: так, вот здесь находится описание заведения, оно мне пока неинтересно и углубляться в эту ветку я пока не буду. Если перемешать данные, которые нужны в моменте одновременно для выполнения действия по разным композитам — это только ухудшит читабельность, а не улучшит.
|
||||
|
||||
Дополнительно правильная декомпозиция поможет нам в решении задачи расширения и развития API, о чем мы поговорим в разделе II.
|
||||
Дополнительно правильная декомпозиция поможет нам в решении задачи расширения и развития API, о чём мы поговорим в разделе II.
|
||||
|
@ -125,13 +125,13 @@ strpbrk (str1, str2)
|
||||
|
||||
**Хорошо**: `"task.is_finished": true`.
|
||||
|
||||
Отдельно следует оговорить, что на разных платформах эти правила следует дополнить по-своему с учетом специфики first-class citizen-типов. Например, объекты типа `Date`, если таковые имеются, разумно индицировать с помощью, например, постфикса `_at` (`created_at`, `occurred_at` и т.д.) или `_date`.
|
||||
Отдельно следует оговорить, что на разных платформах эти правила следует дополнить по-своему с учётом специфики first-class citizen-типов. Например, объекты типа `Date`, если таковые имеются, разумно индицировать с помощью, например, постфикса `_at` (`created_at`, `occurred_at` и т.д.) или `_date`.
|
||||
|
||||
Если наименование сущности само по себе является каким-либо термином, способным смутить разработчика, лучше добавить лишний префикс или постфикс во избежание непонимания.
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
// Возвращает список встроенных функций кофе-машины
|
||||
// Возвращает список встроенных функций кофемашины
|
||||
GET /coffee-machines/{id}/functions
|
||||
```
|
||||
Слово "functions" многозначное: оно может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).
|
||||
@ -157,7 +157,7 @@ strpos(haystack, needle)
|
||||
str_replace(needle, replace, haystack)
|
||||
```
|
||||
Здесь нарушены сразу несколько правил:
|
||||
* написание неконсистентно в части знака подчеркивания;
|
||||
* написание неконсистентно в части знака подчёркивания;
|
||||
* близкие по смыслу методы имеют разный порядок аргументов `needle`/`haystack`;
|
||||
* первый из методов находит только первое вхождение строки `needle`, а другой — все вхождения, и об этом поведении никак нельзя узнать из сигнатуры функций.
|
||||
|
||||
@ -232,7 +232,7 @@ GET /v1/users/{id}/orders
|
||||
— люди в целом плохо считывают двойные отрицания. Это провоцирует ошибки.
|
||||
|
||||
**Лучше**: `"prohibit_calling": true` или `"avoid_calling": true`
|
||||
— читается лучше, хотя обольщаться все равно не следует. Насколько это возможно откажитесь от семантически двойных отрицаний, даже если вы придумали «негативное» слово без явной приставки «не».
|
||||
— читается лучше, хотя обольщаться всё равно не следует. Насколько это возможно откажитесь от семантически двойных отрицаний, даже если вы придумали «негативное» слово без явной приставки «не».
|
||||
|
||||
Стоит также отметить, что в использовании [законов де Моргана](https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BA%D0%BE%D0%BD%D1%8B_%D0%B4%D0%B5_%D0%9C%D0%BE%D1%80%D0%B3%D0%B0%D0%BD%D0%B0) ошибиться ещё проще, чем в двойных отрицаниях. Предположим, что у вас есть два флага:
|
||||
|
||||
@ -720,7 +720,7 @@ GET /v1/price?recipe=lungo
|
||||
GET /v1/records?limit=10&offset=100
|
||||
```
|
||||
На первый взгляд это самый что ни на есть стандартный способ организации пагинации в API. Однако зададим себе три вопроса.
|
||||
1. Каким образом клиент узнает о появлении новых записей в начале списка?
|
||||
1. Каким образом клиент узнает о появлении новых записей в начале списка?
|
||||
Легко заметить, что клиент может только попытаться повторить первый запрос и сверить идентификаторы с запомненным началом списка. Но что делать, если добавленное количество записей превышает `limit`? Представим себе ситуацию:
|
||||
* клиент обрабатывает записи в порядке поступления;
|
||||
* произошла какая-то проблема, и накопилось большое количество необработанных записей;
|
||||
@ -848,7 +848,7 @@ POST /v1/coffee-machines/search
|
||||
→ 400 Bad Request
|
||||
{}
|
||||
```
|
||||
— да, конечно, допущенные ошибки (опечатка в `"lngo"` и неправильные координаты) очевидны. Но раз наш сервер все равно их проверяет, почему не вернуть описание ошибок в читаемом виде?
|
||||
— да, конечно, допущенные ошибки (опечатка в `"lngo"` и неправильные координаты) очевидны. Но раз наш сервер всё равно их проверяет, почему не вернуть описание ошибок в читаемом виде?
|
||||
|
||||
**Хорошо**:
|
||||
```
|
||||
@ -905,7 +905,7 @@ POST /v1/orders
|
||||
"reason": "recipe_unknown"
|
||||
}
|
||||
```
|
||||
— какой был смысл получать новый `offer`, если заказ все равно не может быть создан?
|
||||
— какой был смысл получать новый `offer`, если заказ всё равно не может быть создан?
|
||||
|
||||
**Во-вторых**, соблюдайте такой порядок разрешимых ошибок, который приводит к наименьшему раздражению пользователя и разработчика. В частности, следует начинать с более значимых ошибок, решение которых требует более глобальных изменений.
|
||||
|
||||
@ -934,7 +934,7 @@ POST /v1/orders
|
||||
"localized_message": "Лимит заказов превышен"
|
||||
}
|
||||
```
|
||||
— какой был смысл показывать пользователю диалог об изменившейся цене, если и с правильной ценой заказ он сделать все равно не сможет? Пока один из его предыдущих заказов завершится и можно будет сделать следующий заказ, цену, наличие и другие параметры заказа всё равно придётся корректировать ещё раз.
|
||||
— какой был смысл показывать пользователю диалог об изменившейся цене, если и с правильной ценой заказ он сделать всё равно не сможет? Пока один из его предыдущих заказов завершится и можно будет сделать следующий заказ, цену, наличие и другие параметры заказа всё равно придётся корректировать ещё раз.
|
||||
|
||||
**В-третьих**, постройте таблицу: разрешение какой ошибки может привести к появлению другой, иначе вы можете показать одну и ту же ошибку несколько раз, а то и вовсе зациклить разрешение ошибок.
|
||||
|
||||
@ -974,7 +974,7 @@ POST /v1/orders
|
||||
}
|
||||
```
|
||||
|
||||
Легко заметить, что в этом примере нет способа разрешить ошибку в один шаг — эту ситуацию требуется предусмотреть отдельно, и либо изменить параметры расчета (минимальная сумма заказа не учитывает скидки), либо ввести специальную ошибку для такого кейса.
|
||||
Легко заметить, что в этом примере нет способа разрешить ошибку в один шаг — эту ситуацию требуется предусмотреть отдельно, и либо изменить параметры расчёта (минимальная сумма заказа не учитывает скидки), либо ввести специальную ошибку для такого кейса.
|
||||
|
||||
##### Отсутствие результата — тоже результат
|
||||
|
||||
@ -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" }
|
||||
→
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
2. Что значит «длительное время»?
|
||||
|
||||
С нашей точки зрения длительность поддержания обратной совместимости следует увязывать с длительностью жизненных циклов приложений в соответствующей предметной области. Хороший ориентир в большинстве случаев — это LTS-периоды платформ. Так как приложение все равно будет переписано в связи с окончанием поддержки платформы, нормально предложить также и переход на новую версию API. В основных предметных областях (десктопные и мобильные операционные системы) этот срок исчисляется несколькими годами.
|
||||
С нашей точки зрения длительность поддержания обратной совместимости следует увязывать с длительностью жизненных циклов приложений в соответствующей предметной области. Хороший ориентир в большинстве случаев — это LTS-периоды платформ. Так как приложение всё равно будет переписано в связи с окончанием поддержки платформы, нормально предложить также и переход на новую версию API. В основных предметных областях (десктопные и мобильные операционные системы) этот срок исчисляется несколькими годами.
|
||||
|
||||
Почему обратную совместимость необходимо поддерживать (в том числе предпринимать необходимые меры ещё на этапе проектирования API) — понятно из определения. Прекращение работы приложения (полное или частичное) по вине поставщика API — крайне неприятное событие, а то и катастрофа, для любого разработчика, особенно если он платит за этот API деньги.
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
|
||||
1. Если платформа поддерживает on-demand получение кода, как старый-добрый Веб, и вы не поленились это получение кода реализовать (в виде платформенного SDK, например, JS API), то развитие API более или менее находится под вашим контролем. Поддержание обратной совместимости сводится к поддержанию обратной совместимости *клиентской библиотеки*, а вот в части сервера и клиент-серверного взаимодействия вы свободны.
|
||||
|
||||
Это не означает, что вы не можете нарушить обратную совместимость — всё ещё можно напортачить с заголовками кэширования SDK или банально допустить баг в коде. Кроме того, даже on-demand системы все равно не обновляются мгновенно — автор сталкивался с ситуацией, когда пользователи намеренно держали вкладку браузера открытой *неделями*, чтобы не обновляться на новые версии. Тем не менее, вам почти не придётся поддерживать более двух (последней и предпоследней) минорных версий клиентского SDK. Более того, вы можете попытаться в какой-то момент переписать предыдущую мажорную версию библиотеки, имплементировав её на основе API новой версии.
|
||||
Это не означает, что вы не можете нарушить обратную совместимость — всё ещё можно напортачить с заголовками кэширования SDK или банально допустить баг в коде. Кроме того, даже on-demand системы всё равно не обновляются мгновенно — автор сталкивался с ситуацией, когда пользователи намеренно держали вкладку браузера открытой *неделями*, чтобы не обновляться на новые версии. Тем не менее, вам почти не придётся поддерживать более двух (последней и предпоследней) минорных версий клиентского SDK. Более того, вы можете попытаться в какой-то момент переписать предыдущую мажорную версию библиотеки, имплементировав её на основе API новой версии.
|
||||
|
||||
2. Если поддержка on-demand кода платформой не поддерживается или запрещена условиями, как это произошло с современными мобильными платформами, то ситуация становится гораздо сложнее. По сути, каждый клиент — это «слепок» кода, который работает с вашим API, зафиксированный в том состоянии, в котором он был на момент компиляции. Обновление клиентских приложений по времени растянуто гораздо дольше, нежели Web-приложений; самое неприятное здесь состоит в том, что некоторые клиенты *не обновятся вообще никогда* — по одной из трёх причин:
|
||||
|
||||
@ -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. Какое количество *мажорных* версий поддерживать одновременно.
|
||||
|
||||
|
@ -14,14 +14,14 @@
|
||||
|
||||
##### Избегайте серых зон и недосказанности
|
||||
|
||||
Ваши обязательства по поддержанию функциональности должны быть оговорены настолько четко, насколько это возможно. Особенно это касается тех сред и платформ, где нет способа нативно ограничить доступ к недокументированной функциональности. К сожалению, разработчики часто считают, что, если они «нашли» какую-то непубличную особенность, то они могут ей пользоваться — а производитель API, соответственно, обязан её поддерживать. Поэтому политика компании относительно таких «находок» должна быть явно сформулирована. Тогда в случае несанкционированного использования скрытой функциональности вы по крайней мере сможете сослаться на документацию и быть формально правы в глазах комьюнити.
|
||||
Ваши обязательства по поддержанию функциональности должны быть оговорены настолько чётко, насколько это возможно. Особенно это касается тех сред и платформ, где нет способа нативно ограничить доступ к недокументированной функциональности. К сожалению, разработчики часто считают, что, если они «нашли» какую-то непубличную особенность, то они могут ей пользоваться — а производитель API, соответственно, обязан её поддерживать. Поэтому политика компании относительно таких «находок» должна быть явно сформулирована. Тогда в случае несанкционированного использования скрытой функциональности вы по крайней мере сможете сослаться на документацию и быть формально правы в глазах комьюнити.
|
||||
|
||||
Однако достаточно часто разработчики API сами легитимизируют такие серые зоны, например:
|
||||
|
||||
* отдают недокументированные поля в ответах эндпойнтов;
|
||||
* используют непубличную функциональность в примерах кода — в документации, в ответ на обращения пользователей, в выступлениях на конференциях и т.д.
|
||||
|
||||
Нельзя принять обязательства наполовину. Или вы гарантируете работу этого кода всегда, или не подавайте никаких намеков на то, что такая функциональность существует.
|
||||
Нельзя принять обязательства наполовину. Или вы гарантируете работу этого кода всегда, или не подавайте никаких намёков на то, что такая функциональность существует.
|
||||
|
||||
##### Фиксируйте неявные договорённости
|
||||
|
||||
@ -123,7 +123,7 @@ GET /v1/orders/{id}/events/history
|
||||
|
||||
Допустим, в какой-то момент вы решили надёжным клиентам с хорошей историей заказов предоставлять кофе «в кредит», не дожидаясь подтверждения платежа. Т.е. заказ перейдёт в статус `"preparing_started"`, а может и `"ready"`, вообще без события `"payment_approved"`. Вам может показаться, что это изменение является обратно-совместимым — в самом деле, вы же и не обещали никакого конкретного порядка событий. Но это, конечно, не так.
|
||||
|
||||
Предположим, что у разработчика (вероятно, бизнес-партнера вашей компании) написан какой-то код, выполняющий какую-то полезную бизнес функцию поверх этих событий — например, строит аналитику по затратам и доходам. Вполне логично ожидать, что этот код будет оперировать какой-то машиной состояний, которая будет переходить в то или иное состояние в зависимости от получения или неполучения события. Аналитический код наверняка сломается вследствие изменения порядка событий. В лучшем случае разработчик увидит какие-то исключения и будет вынужден разбираться с причиной; в худшем случае партнер будет оперировать неправильной статистикой неопределённое время, пока не найдёт в ней ошибку.
|
||||
Предположим, что у разработчика (вероятно, бизнес-партнёра вашей компании) написан какой-то код, выполняющий какую-то полезную бизнес функцию поверх этих событий — например, строит аналитику по затратам и доходам. Вполне логично ожидать, что этот код будет оперировать какой-то машиной состояний, которая будет переходить в то или иное состояние в зависимости от получения или неполучения события. Аналитический код наверняка сломается вследствие изменения порядка событий. В лучшем случае разработчик увидит какие-то исключения и будет вынужден разбираться с причиной; в худшем случае партнёр будет оперировать неправильной статистикой неопределённое время, пока не найдёт в ней ошибку.
|
||||
|
||||
Правильным решением было бы во-первых, изначально задокументировать порядок событий и допустимые состояния; во-вторых, продолжать генерировать событие `"payment_approved"` перед `"preparing_started"` (если вы приняли решение исполнять такой заказ — значит, по сути, подтвердили платёж) и добавить расширенную информацию о платеже.
|
||||
|
||||
@ -133,6 +133,6 @@ GET /v1/orders/{id}/events/history
|
||||
|
||||
Такие критичные вещи, как граф переходов между статусами, порядок событий и возможные причины тех или иных изменений — должны быть документированы. Далеко не все детали бизнес-логики можно выразить в форме контрактов на эндпойнты, а некоторые вещи нельзя выразить вовсе.
|
||||
|
||||
Представьте, что в один прекрасный день вы заводите специальный номер телефона, по которому клиент может позвонить в колл-центр и отменить заказ. Вы даже можете сделать это *технически* обратно-совместимым образом, добавив новых необязательных полей в сущность «заказ». Но конечный потребитель может просто *знать* нужный номер телефона, и позвонить по нему, даже если приложение его не показало. При этом код бизнес-аналитика партнера всё так же может сломаться или начать показывать погоду на Марсе, т.к. он был написан когда-то, ничего не зная о возможности отменить заказ, сделанный в приложении партнера, каким-то иным образом, не через самого партнёра же.
|
||||
Представьте, что в один прекрасный день вы заводите специальный номер телефона, по которому клиент может позвонить в колл-центр и отменить заказ. Вы даже можете сделать это *технически* обратно-совместимым образом, добавив новых необязательных полей в сущность «заказ». Но конечный потребитель может просто *знать* нужный номер телефона, и позвонить по нему, даже если приложение его не показало. При этом код бизнес-аналитика партнёра всё так же может сломаться или начать показывать погоду на Марсе, т.к. он был написан когда-то, ничего не зная о возможности отменить заказ, сделанный в приложении партнёра, каким-то иным образом, не через самого партнёра же.
|
||||
|
||||
*Технически* корректным решением в данной ситуации могло бы быть добавление параметра «разрешено отменять через колл-центр» в функцию создания заказа — и, соответственно, запрет операторам колл-центра отменять заказы, если флаг не был указан при их создании. Но это в свою очередь плохое решение *с точки зрения продукта*. «Хорошее» решение здесь только одно — изначально предусмотреть возможность внешних отмен в API; если же вы её не предвидели — остаётся воспользоваться «блокнотом душевного спокойствия», речь о котором пойдёт в последней главе настоящего раздела.
|
||||
|
@ -2,15 +2,15 @@
|
||||
|
||||
В предыдущих разделах мы старались приводить теоретические правила и иллюстрировать их на практических примерах. Однако понимание принципов проектирования API, устойчивого к изменениям, как ничто другое требует прежде всего практики. Знание о том, куда стоит «постелить соломку» — оно во многом «сын ошибок трудных». Нельзя предусмотреть всего — но можно выработать необходимый уровень технической интуиции.
|
||||
|
||||
Поэтому в этом разделе мы поступим следующим образом: возьмём наш [модельный API](#chapter-12) из предыдущего раздела, и проверим его на устойчивость в каждой возможной точке — проведём некоторый «вариационный анализ» наших интерфейсов. Ещё более конкретно — к каждой сущности мы подойдём с вопросом «что, если?» — что, если нам потребуется предоставить партнерам возможность написать свою независимую реализацию этого фрагмента логики.
|
||||
Поэтому в этом разделе мы поступим следующим образом: возьмём наш [модельный API](#chapter-12) из предыдущего раздела, и проверим его на устойчивость в каждой возможной точке — проведём некоторый «вариационный анализ» наших интерфейсов. Ещё более конкретно — к каждой сущности мы подойдём с вопросом «что, если?» — что, если нам потребуется предоставить партнёрам возможность написать свою независимую реализацию этого фрагмента логики.
|
||||
|
||||
**NB**. В рассматриваемых нами примерах мы будем выстраивать интерфейсы так, чтобы связывание разных сущностей происходило динамически в реальном времени; на практике такие интеграции будут делаться на стороне сервера путём написания ad hoc кода и формирования конкретных договорённостей с конкретным клиентом, однако мы для целей обучения специально будем идти более сложным и абстрактным путём. Динамическое связывание в реальном времени применимо скорее к сложным программным конструктам типа API операционных систем или встраиваемых библиотек; приводить обучающие примеры на основе систем подобной сложности было бы, однако, чересчур затруднительно.
|
||||
|
||||
Начнём с базового интерфейса. Предположим, что мы пока что вообще не раскрывали никакой функциональности помимо поиска предложений и заказа, т.е. мы предоставляем 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`:
|
||||
```
|
||||
@ -82,11 +82,11 @@ PUT /v1/partners/{partnerId}/coffee-machines
|
||||
|
||||
Часто вместо добавления нового метода можно добавить просто необязательный параметр к существующему интерфейсу — в нашем случае, можно добавить необязательный параметр `options` к вызову `PUT /cofee-machines`.
|
||||
|
||||
**NB**. Когда мы говорим о фиксации договоренностей, действующих в настоящий момент — речь идёт о *внутренних* договорённостях. Мы должны были потребовать от партнеров поддерживать указанный список опций, когда обговаривали формат взаимодействия. Если же мы этого не сделали изначально, а потом решили зафиксировать договорённости в ходе расширения функциональности внешнего API — это очень серьёзная заявка на нарушение обратной совместимости, и так делать ни в коем случае не надо, см. [главу 14](#chapter-14).
|
||||
**NB**. Когда мы говорим о фиксации договоренностей, действующих в настоящий момент — речь идёт о *внутренних* договорённостях. Мы должны были потребовать от партнёров поддерживать указанный список опций, когда обговаривали формат взаимодействия. Если же мы этого не сделали изначально, а потом решили зафиксировать договорённости в ходе расширения функциональности внешнего API — это очень серьёзная заявка на нарушение обратной совместимости, и так делать ни в коем случае не надо, см. [главу 14](#chapter-14).
|
||||
|
||||
#### Границы применимости
|
||||
|
||||
Хотя это упражнение выглядит весьма простым и универсальным, его использование возможно только при наличии хорошо продуманной архитектуры сущностей и, что ещё более важного, понятного вектора дальнейшего развития API. Представим, что через какое-то время к поддерживаемым опциям добавились новые — ну, скажем, добавление сиропа и второго шота эспрессо. Список опций расширить мы можем — а вот изменить соглашение по умолчанию уже нет. Через некоторое время это приведёт к тому, что «дефолтный» интерфейс `PUT /coffee-machines` окажется никому не нужен, поскольку «дефолтный» список из трёх опций окажется не только редко востребованным, но и просто абсурдным — почему эти три, чем они лучше всех остальных? По сути значения по умолчанию и номенклатура старых методов начнут отражать исторические этапы развития нашего API, а это совершенно не то, чего мы хотели бы от номенклатуры хелперов и значений по умолчанию.
|
||||
Хотя это упражнение выглядит весьма простым и универсальным, его использование возможно только при наличии хорошо продуманной архитектуры сущностей и, что ещё более важно, понятного вектора дальнейшего развития API. Представим, что через какое-то время к поддерживаемым опциям добавились новые — ну, скажем, добавление сиропа и второго шота эспрессо. Список опций расширить мы можем — а вот изменить соглашение по умолчанию уже нет. Через некоторое время это приведёт к тому, что «дефолтный» интерфейс `PUT /coffee-machines` окажется никому не нужен, поскольку «дефолтный» список из трёх опций окажется не только редко востребованным, но и просто абсурдным — почему эти три, чем они лучше всех остальных? По сути значения по умолчанию и номенклатура старых методов начнут отражать исторические этапы развития нашего API, а это совершенно не то, чего мы хотели бы от номенклатуры хелперов и значений по умолчанию.
|
||||
|
||||
Увы, здесь мы сталкиваемся с плохо разрешимым противоречием: мы хотим, с одной стороны, чтобы разработчик писал лаконичный код, следовательно, должны предоставлять хорошие хелперные методы и значения по умолчанию. С другой, знать наперёд какими будут самые частотные наборы опций через несколько лет развития API — очень сложно.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
### Сильная связность и сопутствующие проблемы
|
||||
|
||||
Для демонстрации проблем сильной связности перейдём теперь к *действительно интересным* вещам. Продолжим наш «вариационный анализ»: что, если партнёры хотят не просто готовить кофе по стандартным рецептам, но и предлагать свои авторские напитки? Вопрос этот с подвохом: в том виде, как мы описали партнёрскый API в предыдущей главе, факт существования партнерской сети никак не отражен в нашем API с точки зрения продукта, предлагаемого пользователю, а потому представляет собой довольно простой кейс. Если же мы пытаемся предоставить не какую-то дополнительную возможность, а модифицировать саму базовую функциональность API, то мы быстро столкнёмся с проблемами совсем другого порядка.
|
||||
Для демонстрации проблем сильной связности перейдём теперь к *действительно интересным* вещам. Продолжим наш «вариационный анализ»: что, если партнёры хотят не просто готовить кофе по стандартным рецептам, но и предлагать свои авторские напитки? Вопрос этот с подвохом: в том виде, как мы описали партнёрскый API в предыдущей главе, факт существования партнёрской сети никак не отражён в нашем API с точки зрения продукта, предлагаемого пользователю, а потому представляет собой довольно простой кейс. Если же мы пытаемся предоставить не какую-то дополнительную возможность, а модифицировать саму базовую функциональность API, то мы быстро столкнёмся с проблемами совсем другого порядка.
|
||||
|
||||
Итак, добавим ещё один эндпойнт — для регистрации собственного рецепта партнёра.
|
||||
|
||||
@ -48,7 +48,7 @@ POST /v1/recipes
|
||||
|
||||
Корректный ответ — потому что существует некоторое представление, UI для выбора типа напитка. По-видимому, `name` и `description` — это просто два описания напитка, короткое (для показа в общем прейскуранте) и длинное (для показа расширенной информации о продукте). Получается, что мы устанавливаем требования на API исходя из вполне конкретного дизайна. Но что, если партнёр сам делает UI для своего приложения? Мало того, что ему могут быть не нужны два описания, так мы по сути ещё и вводим его в заблуждение: `name` — это не «какое-то» название, оно предполагает некоторые ограничения. Во-первых, у него есть некоторая рекомендованная длина, оптимальная для конкретного UI; во-вторых, оно должно консистентно выглядеть в одном списке с другими напитками. В самом деле, будет очень странно смотреться, если среди «Капучино», «Лунго» и «Латте» вдруг появится «Бодрящая свежесть» или «Наш самый качественный кофе».
|
||||
|
||||
Эта проблема разворачивается и в другую сторону — UI (наш или партнера) обязательно будет развиваться, в нём будут появляться новые элементы (картинка для кофе, его пищевая ценность, информация об аллергенах и так далее). `product_properties` со временем превратится в свалку из большого количества необязательных полей, и выяснить, задание каких из них приведёт к каким эффектам в каком приложении можно будет только методом проб и ошибок.
|
||||
Эта проблема разворачивается и в другую сторону — UI (наш или партнёра) обязательно будет развиваться, в нём будут появляться новые элементы (картинка для кофе, его пищевая ценность, информация об аллергенах и так далее). `product_properties` со временем превратится в свалку из большого количества необязательных полей, и выяснить, задание каких из них приведёт к каким эффектам в каком приложении можно будет только методом проб и ошибок.
|
||||
|
||||
Проблемы, с которыми мы столкнулись — это проблемы *сильной связности*. Каждый раз, предлагая интерфейс, подобный вышеприведённому, мы фактически описываем имплементацию одной сущности (рецепта) через имплементации других (визуального макета, правил локализации). Этот подход противоречит самому принципу проектирования API «сверху вниз», поскольку **низкоуровневые сущности не должны определять высокоуровневые**.
|
||||
|
||||
@ -64,7 +64,7 @@ l10n.volume.format(value, language_code, country_code)
|
||||
// l10n.formatVolume('300ml', 'en', 'US') → '10 fl oz'
|
||||
```
|
||||
|
||||
Чтобы наш API корректно заработал с новым языком или регионом, партнер должен или задать эту функцию, или указать, какую из существующих локализаций необходимо использовать. Для этого мы абстрагируем-и-расширяем API, в соответствии с описанной в предыдущей главе процедурой, и добавляем новый эндпойнт — настройки форматирования:
|
||||
Чтобы наш API корректно заработал с новым языком или регионом, партнёр должен или задать эту функцию, или указать, какую из существующих локализаций необходимо использовать. Для этого мы абстрагируем-и-расширяем API, в соответствии с описанной в предыдущей главе процедурой, и добавляем новый эндпойнт — настройки форматирования:
|
||||
|
||||
```
|
||||
// Добавляем общее правило форматирования
|
||||
@ -97,7 +97,7 @@ GET /v1/layouts/{layout_id}
|
||||
"id",
|
||||
// Макетов вполне возможно будет много разных,
|
||||
// поэтому имеет смысл сразу заложить
|
||||
// расширяемоесть
|
||||
// расширяемость
|
||||
"kind": "recipe_search",
|
||||
// Описываем каждое свойство рецепта,
|
||||
// которое должно быть задано для
|
||||
@ -181,7 +181,7 @@ POST /v1/recipe-builder
|
||||
}
|
||||
```
|
||||
|
||||
Заметим, что передача идентификатора вновь создаваемой сущности клиентом — не лучший паттерн. Но раз уж мы с самого начала решили, что идентификаторы рецептов — не просто случайные наборы символов, а значимые строки, то нам теперь придётся с этим как-то жить. Очевидно, в такой ситуации мы рискуем многочисленными коллизиями между названиями рецептов разных партнёров, поэтому операцию, на самом деле, следует модифицировать: либо для партнерских рецептов всегда пользоваться парой идентификаторов (партнера и рецепта), либо ввести составные идентификаторы, как мы ранее рекомендовали в [главе 11](#chapter-11-paragraph-8).
|
||||
Заметим, что передача идентификатора вновь создаваемой сущности клиентом — не лучший паттерн. Но раз уж мы с самого начала решили, что идентификаторы рецептов — не просто случайные наборы символов, а значимые строки, то нам теперь придётся с этим как-то жить. Очевидно, в такой ситуации мы рискуем многочисленными коллизиями между названиями рецептов разных партнёров, поэтому операцию, на самом деле, следует модифицировать: либо для партнёрских рецептов всегда пользоваться парой идентификаторов (партнёра и рецепта), либо ввести составные идентификаторы, как мы ранее рекомендовали в [главе 11](#chapter-11-paragraph-8).
|
||||
|
||||
```
|
||||
POST /v1/recipes/custom
|
||||
|
@ -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,10 +87,10 @@ PUT /v1/api-types/{api_type}
|
||||
|
||||
```
|
||||
/* Имплементация партнёром интерфейса
|
||||
запуска программы на его кофе-машинах */
|
||||
запуска программы на его кофемашинах */
|
||||
registerProgramRunHandler(apiType, (context) => {
|
||||
// Инициализируем запуск исполнения
|
||||
// программы на стороне партнера
|
||||
// программы на стороне партнёра
|
||||
let execution = initExecution(context, …);
|
||||
// Подписываемся на события
|
||||
// изменения контекста
|
||||
@ -112,7 +112,7 @@ registerProgramRunHandler(apiType, (context) => {
|
||||
|
||||
Внимательный читатель может возразить нам, что фактически, если мы посмотрим на номенклатуру возникающих сущностей, мы ничего не изменили в постановке задачи, и даже усложнили её:
|
||||
* вместо вызова метода `takeout` мы теперь генерируем пару событий `takeout_requested`/`takeout_ready`;
|
||||
* вместо длинного списка методов, которые необходимо реализовать для интеграции API партнера, появляются длинные списки полей сущности `context` и событий, которые она генерирует;
|
||||
* вместо длинного списка методов, которые необходимо реализовать для интеграции API партнёра, появляются длинные списки полей сущности `context` и событий, которые она генерирует;
|
||||
* проблема устаревания технологии не меняется, вместо устаревших методов мы теперь имеем устаревшие поля и события.
|
||||
|
||||
Это замечание совершенно верно. Изменение формата API само по себе не решает проблем, связанных с эволюцией функциональности и нижележащей технологии. Формат API решает другую проблему: как оставить при этом код читаемым и поддерживаемым. Почему в примере с интеграцией через методы код становится нечитаемым? Потому что обе стороны *вынуждены* имплементировать функциональность, которая в их контексте бессмысленна; и эта имплементация будет состоять из какого-то (хорошо если явного!) способа ответить, что данная функциональность не поддерживается (или, наоборот, поддерживается всегда и безусловно).
|
||||
@ -125,7 +125,7 @@ registerProgramRunHandler(apiType, (context) => {
|
||||
|
||||
Важно также отметить, что, хотя количество сущностей (полей, событий) эффективно удваивается по сравнению с сильно связанным API, это удвоение является качественным, а не количественным. Контекст `program` содержит описание задания в своих терминах (вид напитка, объём, посыпка корицей); контекст `execution` должен эти термины переформулировать для своей предметной области (чтобы быть, в свою очередь, таким же информационным контекстом для ещё более низкоуровневого API). Что важно, `execution`-контекст имеет право эти термины конкретизировать, поскольку его нижележащие объекты будут уже работать в рамках какого-то конкретного API, в то время как `program`-контекст обязан выражаться в общих терминах, применимых к любой возможной нижележащей технологии.
|
||||
|
||||
Ещё одним важным свойством слабой связности является то, что она позволяет сущности иметь несколько родительских контекстов. В обычных предметных областях такая ситуация выглядела бы ошибкой дизайна API, но в сложных системах, где присутствуют одновременно несколько агентов, влияющих на состояние системы, такая ситуация не является редкостью. В частности, вы почти наверняка столкнётесь с такого рода проблемами при разработке пользовательского UI. Более подробно о подобных двойных иерархиях мы расскажем в разделе, посвященном разработке SDK.
|
||||
Ещё одним важным свойством слабой связности является то, что она позволяет сущности иметь несколько родительских контекстов. В обычных предметных областях такая ситуация выглядела бы ошибкой дизайна API, но в сложных системах, где присутствуют одновременно несколько агентов, влияющих на состояние системы, такая ситуация не является редкостью. В частности, вы почти наверняка столкнётесь с такого рода проблемами при разработке пользовательского UI. Более подробно о подобных двойных иерархиях мы расскажем в разделе, посвящённом разработке SDK.
|
||||
|
||||
#### Инверсия ответственности
|
||||
|
||||
@ -133,10 +133,10 @@ registerProgramRunHandler(apiType, (context) => {
|
||||
|
||||
```
|
||||
/* Имплементация партнёром интерфейса
|
||||
запуска программы на его кофе-машинах */
|
||||
запуска программы на его кофемашинах */
|
||||
registerProgramRunHandler(apiType, (context) => {
|
||||
// Инициализируем запуск исполнения
|
||||
// программы на стороне партнера
|
||||
// программы на стороне партнёра
|
||||
let execution = initExecution(context, …);
|
||||
// Подписываемся на события
|
||||
// изменения контекста
|
||||
@ -159,10 +159,10 @@ registerProgramRunHandler(apiType, (context) => {
|
||||
}
|
||||
```
|
||||
|
||||
Вновь такое решение выглядит контринтуитивным, ведь мы снова вернулись к сильной связи двух уровней через жестко определённые методы. Однако здесь есть важный момент: мы городим весь этот огород потому, что ожидаем появления альтернативных реализаций *нижележащего* уровня абстракции. Ситуации, когда появляются альтернативные реализации *вышележащего* уровня абстракции, конечно, возможны, но крайне редки. Обычно дерево альтернативных реализаций растёт сверху вниз.
|
||||
Вновь такое решение выглядит контринтуитивным, ведь мы снова вернулись к сильной связи двух уровней через жёстко определённые методы. Однако здесь есть важный момент: мы городим весь этот огород потому, что ожидаем появления альтернативных реализаций *нижележащего* уровня абстракции. Ситуации, когда появляются альтернативные реализации *вышележащего* уровня абстракции, конечно, возможны, но крайне редки. Обычно дерево альтернативных реализаций растёт сверху вниз.
|
||||
|
||||
Другой аспект заключается в том, что, хотя серьёзные изменения концепции возможны на любом из уровней абстракции, их вес принципиально разный:
|
||||
* если меняется технический уровень, это не должно существенно влиять на продукт, а значит — на написанный партнерами код;
|
||||
* если меняется технический уровень, это не должно существенно влиять на продукт, а значит — на написанный партнёрами код;
|
||||
* если меняется сам продукт, ну например мы начинаем продавать билеты на самолёт вместо приготовления кофе на заказ, сохранять обратную совместимость на промежуточных уровнях API *бесполезно*. Мы вполне можем продавать билеты на самолёт тем же самым API программ и контекстов, да только написанный партнёрами код всё равно надо будет полностью переписывать с нуля.
|
||||
|
||||
В конечном итоге это приводит к тому, что API вышележащих сущностей меняется медленнее и более последовательно по сравнению с API нижележащих уровней, а значит подобного рода «обратная» жёсткая связь зачастую вполне допустима и даже желательна исходя из соотношения «цена-качество».
|
||||
@ -179,7 +179,7 @@ execution.prepareTakeout(() => {
|
||||
});
|
||||
```
|
||||
|
||||
Надо отметить, что такой подход *в принципе* не противоречит описанному принципу, но нарушает другой — изоляцию уровней абстракции, а поэтому плохо подходит для написания сложных API, в которых не гарантирована жесткая иерархия компонентов. При этом использовать глобальный (или квази-глобальный) менеджер состояния в таких системах вполне возможно, но требуется имплементировать более сложную пропагацию сообщений по иерархии, а именно: подчинённый объект всегда вызывает методы только ближайшего вышестоящего объекта, а уже тот решает, как и каким образом этот вызов передать выше по иерархии.
|
||||
Надо отметить, что такой подход *в принципе* не противоречит описанному принципу, но нарушает другой — изоляцию уровней абстракции, а поэтому плохо подходит для написания сложных API, в которых не гарантирована жёсткая иерархия компонентов. При этом использовать глобальный (или квази-глобальный) менеджер состояния в таких системах вполне возможно, но требуется имплементировать более сложную пропагацию сообщений по иерархии, а именно: подчинённый объект всегда вызывает методы только ближайшего вышестоящего объекта, а уже тот решает, как и каким образом этот вызов передать выше по иерархии.
|
||||
|
||||
```
|
||||
execution.prepareTakeout(() => {
|
||||
@ -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…
Reference in New Issue
Block a user