mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-03-17 20:42:26 +02:00
Merge pull request #21 from Callista-del-Mar/fix-DesignAPI-ru
Fix design api ru
This commit is contained in:
commit
788adfdcd4
@ -8,6 +8,6 @@
|
||||
|
||||
Этот алгоритм строит API сверху вниз, от общих требований и сценариев использования до конкретной номенклатуры сущностей; фактически, двигаясь этим путем, вы получите на выходе готовое API — чем этот подход и ценен.
|
||||
|
||||
Может показаться, что наиболее полезные советы приведены в последнем разделе, однако это не так; цена ошибки, допущенной на разных уровнях весьма различна. Если исправить плохое именование довольно просто, то исправить неверное понимание того, зачем вообще нужно API, практически невозможно.
|
||||
Может показаться, что наиболее полезные советы приведены в последнем разделе, однако это не так; цена ошибки, допущенной на разных уровнях весьма различна. Если исправить плохое именование довольно просто, то исправить неверное понимание того, зачем вообще нужен API, практически невозможно.
|
||||
|
||||
**NB**. Здесь и далее мы будем рассматривать концепции разработки API на примере некоторого гипотетического API заказа кофе в городских кофейнях. На всякий случай сразу уточним, что пример является синтетическим; в реальной ситуации, если бы такой API пришлось проектировать, он, вероятно, был бы совсем не похож на наш выдуманный пример.
|
||||
**NB**. Здесь и далее мы будем рассматривать концепции разработки API на примере некоторого гипотетического API заказа кофе в городских кофейнях. На всякий случай сразу уточним, что пример является синтетическим; в реальной ситуации, если бы такой API пришлось проектировать, он, вероятно, был бы совсем не похож на наш выдуманный пример.
|
||||
|
@ -32,7 +32,7 @@
|
||||
|
||||
#### Почему API?
|
||||
|
||||
Т.к. наша книга посвящена не просто разработке программного обеспечения, а разработке API, то на все эти вопросы мы должны взглянуть под другим ракурсом: а почему для решения этих задач требуется именно API, а не просто программное обеспечение? В нашем вымышленном примере мы должны спросить себя: зачем нам нужно предоставлять сервис для других разработчиков, чтобы они могли готовить кофе своим клиентам, а не сделать своё приложение для конечного потребителя?
|
||||
Поскольку наша книга посвящена не просто разработке программного обеспечения, а разработке API, то на все эти вопросы мы должны взглянуть под другим ракурсом: а почему для решения этих задач требуется именно API, а не просто программное обеспечение? В нашем вымышленном примере мы должны спросить себя: зачем нам нужно предоставлять сервис для других разработчиков, чтобы они могли готовить кофе своим клиентам, а не сделать своё приложение для конечного потребителя?
|
||||
|
||||
Иными словами, должна иметься веская причина, по которой два домена разработки ПО должны быть разделены: есть оператор(ы), предоставляющий API; есть оператор(ы), предоставляющий сервисы пользователям. Их интересы в чем-то различны настолько, что объединение этих двух ролей в одном лице нежелательно. Более подробно мы изложим причины и мотивации делать именно API в разделе III.
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
2. Каждый стакан кофе приготовлен по определённому *рецепту*, что подразумевает наличие разных ингредиентов и последовательности выполнения шагов приготовления.
|
||||
3. Напиток готовится на конкретной физической *кофе-машине*, располагающейся в какой-то точке пространства.
|
||||
|
||||
Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы прежде всего стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей.
|
||||
Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы, прежде всего, стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей.
|
||||
|
||||
1. Упрощение работы разработчика и легкость обучения: в каждый момент времени разработчику достаточно будет оперировать только теми сущностями, которые нужны для решения его задачи; и наоборот, плохо выстроенная изоляция приводит к тому, что разработчику нужно держать в голове множество концепций, не имеющих прямого отношения к решаемой задаче.
|
||||
|
||||
@ -306,7 +306,7 @@ POST /v1/runtimes
|
||||
"command_sequence_id",
|
||||
// Чем закончилось исполнение программы
|
||||
// (необязательное)
|
||||
// * "success" — напиток приготовлен и взят
|
||||
// * "success" — напиток приготовлен и выдан
|
||||
// * "terminated" — исполнение остановлено
|
||||
// * "technical_error" — ошибка при приготовлении
|
||||
// * "waiting_time_exceeded" — готовый заказ был
|
||||
@ -322,7 +322,7 @@ POST /v1/runtimes
|
||||
* либо обработчик `POST /orders` сам обращается к доступной информации о рецепте, кофе-машине и программе и формирует stateless-запрос, в котором указаны все нужные данные (тип API кофе-машины и список команд в частности);
|
||||
* либо в запросе содержатся только идентификаторы, и следующие обработчики в цепочке сами обратятся за нужными данными через какие-то внутренние API.
|
||||
|
||||
Оба варианта имеют право на жизнь; какой из них выбрать — зависит от деталей реализации.
|
||||
Оба варианта имеют право на жизнь; какой из них выбрать зависит от деталей реализации.
|
||||
|
||||
#### Изоляция уровней абстракции
|
||||
|
||||
@ -334,14 +334,14 @@ POST /v1/runtimes
|
||||
* обработчик `runs` в свою очередь выполнит операции своего уровня (в частности, проверит тип API кофе-машины) и в зависимости от типа API пойдёт по одной из двух веток исполнения:
|
||||
* либо вызовет `GET /execution/status` физического API кофе-машины, получит объём кофе и сличит с эталонным;
|
||||
* либо обратится к `GET /v1/runtimes/{runtime_id}`, получит `state.status` и преобразует его к статусу заказа;
|
||||
* в случае API второго типа цепочка продолжится: обработчик `GET /runtimes` обратится к физическому API `GET /sensors` и произведёт ряд манипуляций: сличит объём стакана / молотого кофе / налитой воды с запрошенным и при необходимости изменит состояние и статус.
|
||||
* в случае API второго типа цепочка продолжится: обработчик `GET /runtimes` обратится к физическому API `GET /sensors` и произведёт ряд манипуляций: сопоставит объём стакана / молотого кофе / налитой воды с запрошенным и при необходимости изменит состояние и статус.
|
||||
|
||||
**NB**: слова «цепочка вызовов» не следует воспринимать буквально. Каждый уровень может быть технически организован по-разному:
|
||||
* можно явно проксировать все вызовы по иерархии;
|
||||
* можно кэшировать статус своего уровня и обновлять его по получению обратного вызова или события.
|
||||
* можно кэшировать статус своего уровня и обновлять его по получении обратного вызова или события.
|
||||
В частности, низкоуровневый цикл исполнения рантайма для машин второго рода очевидно должен быть независимым и обновлять свой статус в фоне, не дожидаясь явного запроса статуса.
|
||||
|
||||
Обратите внимание, что здесь фактически происходит следующее: на каждом уровне абстракции есть какой-то свой статус (заказа, рантайма, сенсоров), который сформулирован в терминах соответствующей этому уровню абстракции предметной области. Запрет «перепрыгивания» уровней приводит к тому, что нам необходимо дублировать статус на каждом уровне независимо.
|
||||
Обратите внимание, что здесь фактически происходит следующее: на каждом уровне абстракции есть какой-то свой статус (заказа, рантайма, сенсоров), который сформулирован в терминах соответствующей этому уровню абстракции предметной области. Запрет «перепрыгивания» уровней приводит к тому, что нам необходимо независимо дублировать статус на каждом уровне.
|
||||
|
||||
Рассмотрим теперь, каким образом через наши уровни абстракции «прорастёт» операция отмены заказа. В этом случае цепочка вызовов будет такой:
|
||||
|
||||
@ -368,7 +368,7 @@ POST /v1/runtimes
|
||||
|
||||
Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы оно выполняло свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой.
|
||||
|
||||
Выделение уровней абстракции — прежде всего _логическая_ процедура: как мы объясняем себе и разработчику, из чего состоит наш API. **Абстрагируемая дистанция между сущностями существует объективно**, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни _явно_. Чем более неявно разведены (или, хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API, и тем хуже будет написан использующий его код.
|
||||
Выделение уровней абстракции, прежде всего, _логическая_ процедура: как мы объясняем себе и разработчику, из чего состоит наш API. **Абстрагируемая дистанция между сущностями существует объективно**, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни _явно_. Чем более неявно разведены (или, хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API, и тем хуже будет написан использующий его код.
|
||||
|
||||
#### Потоки данных
|
||||
|
||||
@ -399,4 +399,4 @@ POST /v1/runtimes
|
||||
* с одной стороны, в контексте заказа оказываются данные (объём кофе), «просочившиеся» откуда-то с физического уровня; тем самым, уровни абстракции непоправимо смешиваются без возможности их разделить;
|
||||
* с другой стороны, сам контекст заказа неполноценный: он не задаёт новых мета-переменных, которые отсутствуют на более низких уровнях абстракции (статус заказа), не инициализирует их и не предоставляет правил работы.
|
||||
|
||||
Более подробно о контекстах данных мы поговорим в разделе II. Здесь же ограничимся следующим выводом: потоки данных и их преобразования можно и нужно рассматривать как некоторый срез, который, с одной стороны, помогает нам правильно разделить уровни абстракции, а с другой — проверить, что наши теоретические построения действительно работают так, как нужно.
|
||||
Более подробно о контекстах данных мы поговорим в разделе II. Здесь же ограничимся следующим выводом: потоки данных и их преобразования можно и нужно рассматривать как некоторый срез, который, с одной стороны, помогает нам правильно разделить уровни абстракции, а с другой — проверить, что наши теоретические построения действительно работают так, как нужно.
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Исходя из описанного в предыдущей главе, мы понимаем, что иерархия абстракций в нашем гипотетическом проекте должна выглядеть примерно так:
|
||||
|
||||
* пользовательский уровень (те сущности, с которыми непосредственно взаимодействует пользователь и сформулированы в понятных для него терминах; например, заказы и виды кофе);
|
||||
* пользовательский уровень (те сущности, с которыми непосредственно взаимодействует пользователь, сформулированы в понятных для него терминах; например, заказы и виды кофе);
|
||||
* уровень исполнения программ (те сущности, которые отвечают за преобразование заказа в машинные термины);
|
||||
* уровень рантайма для API второго типа (сущности, отвечающие за state-машину выполнения заказа).
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
В нашем умозрительном примере получится примерно так:
|
||||
|
||||
1. Сущности уровня пользователя (те, работая с которыми, разработчик непосредственно решает задачи пользователя).
|
||||
1. Сущности уровня пользователя (те сущности, работая с которыми, разработчик непосредственно решает задачи пользователя).
|
||||
* Заказ `order` — описывает некоторую логическую единицу взаимодействия с пользователем. Заказ можно:
|
||||
* создавать;
|
||||
* проверять статус;
|
||||
@ -20,14 +20,14 @@
|
||||
* отменять.
|
||||
* Рецепт `recipe` — описывает «идеальную модель» вида кофе, его потребительские свойства. Рецепт в данном контексте для нас неизменяемая сущность, которую можно только просмотреть и выбрать.
|
||||
* Кофе-машина `coffee-machine` — модель объекта реального мира. Из описания кофе-машины мы, в частности, должны извлечь её положение в пространстве и предоставляемые опции (о чём подробнее поговорим ниже).
|
||||
2. Сущности уровня управления исполнением (те, работая с которыми, можно непосредственно исполнить заказ).
|
||||
2. Сущности уровня управления исполнением (те сущности, работая с которыми, можно непосредственно исполнить заказ).
|
||||
* Программа `program` — описывает некоторый план исполнения для конкретной кофе-машины. Программы можно только просмотреть.
|
||||
* Селектор программ `programs/matcher` — позволяет связать рецепт и программу исполнения, т.е. фактически выяснить набор данных, необходимых для приготовления конкретного рецепта на конкретной кофе-машине. Селектор работает только на выбор нужной программы.
|
||||
* Запуск программы `programs/run` — конкретный факт исполнения программы на конкретной кофе-машине. Запуски можно:
|
||||
* инициировать (создавать);
|
||||
* проверять состояние запуска;
|
||||
* отменять.
|
||||
3. Сущности уровня программ исполнения (те, работая с которыми, можно непосредственно управлять состоянием кофе-машины через API второго типа).
|
||||
3. Сущности уровня программ исполнения (те сущности, работая с которыми, можно непосредственно управлять состоянием кофе-машины через API второго типа).
|
||||
* Рантайм `runtime` — контекст исполнения программы, т.е. состояние всех переменных. Рантаймы можно:
|
||||
* создавать;
|
||||
* проверять статус;
|
||||
@ -37,7 +37,7 @@
|
||||
|
||||
#### Сценарии использования
|
||||
|
||||
На этом уровне, когда наше API уже в целом понятно устроено и спроектировано, мы должны поставить себя на место разработчика и попробовать написать код. Наша задача — взглянуть на номенклатуру сущностей и понять, как ими будут пользоваться.
|
||||
На этом уровне, когда наше API уже в целом понятно устроено и спроектировано, мы должны поставить себя на место разработчика и попробовать написать код. Наша задача: взглянуть на номенклатуру сущностей и понять, как ими будут пользоваться.
|
||||
|
||||
Представим, что нам поставили задачу, пользуясь нашим кофейным API, разработать приложение для заказа кофе. Какой код мы напишем?
|
||||
|
||||
@ -197,7 +197,7 @@ POST /v1/orders
|
||||
|
||||
При этом существует «золотое правило», применимое не только к API, но ко множеству других областей проектирования: человек комфортно удерживает в краткосрочной памяти 7±2 различных объекта. Манипулировать большим числом сущностей человеку уже сложно. Это правило также известно как [«закон Миллера»](https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B1%D0%BE%D1%87%D0%B0%D1%8F_%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D1%8C#%D0%9E%D1%86%D0%B5%D0%BD%D0%BA%D0%B0_%D0%B5%D0%BC%D0%BA%D0%BE%D1%81%D1%82%D0%B8_%D1%80%D0%B0%D0%B1%D0%BE%D1%87%D0%B5%D0%B9_%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D0%B8).
|
||||
|
||||
Бороться с этим законом можно только одним способом: декомпозицией. На каждом уровне работы с вашим API нужно стремиться там, где это возможно, логически группировать сущности под одним именем — так, чтобы разработчику никогда не приходилось оперировать более чем 10 сущностями одновременно.
|
||||
Бороться с этим законом можно только одним способом: декомпозицией. На каждом уровне работы с вашим API нужно стремиться логически группировать сущности под одним именем там, где это возможно и таким образом, чтобы разработчику никогда не приходилось оперировать более чем 10 сущностями одновременно.
|
||||
|
||||
Рассмотрим простой пример: что должна возвращать функция поиска подходящей кофе-машины. Для обеспечения хорошего UX приложения необходимо передать довольно значительные объёмы информации.
|
||||
```
|
||||
@ -283,6 +283,6 @@ POST /v1/orders
|
||||
```
|
||||
Такое API читать и воспринимать гораздо удобнее, нежели сплошную простыню различных атрибутов. Более того, возможно, стоит на будущее сразу дополнительно сгруппировать, например, `place` и `route` в одну структуру `location`, или `offer` и `pricing` в одну более общую структуру.
|
||||
|
||||
Важно, что читабельность достигается не просто снижением количества сущностей на одном уровне. Декомпозиция должна производиться таким образом, чтобы разработчик при чтении интерфейса сразу понимал: так, вот здесь находится описание заведения, оно мне пока неинтересно и углубляться в эту ветку я пока не буду. Если перемешать данные, которые одновременно в моменте нужны для выполнения действия, по разным композитам — это только ухудшит читабельность, а не улучшит.
|
||||
Важно, что читабельность достигается не просто снижением количества сущностей на одном уровне. Декомпозиция должна производиться таким образом, чтобы разработчик при чтении интерфейса сразу понимал: так, вот здесь находится описание заведения, оно мне пока неинтересно и углубляться в эту ветку я пока не буду. Если перемешать данные, которые нужны в моменте одновременно для выполнения действия по разным композитам — это только ухудшит читабельность, а не улучшит.
|
||||
|
||||
Дополнительно правильная декомпозиция поможет нам в решении задачи расширения и развития API, о чем мы поговорим в разделе II.
|
||||
Дополнительно правильная декомпозиция поможет нам в решении задачи расширения и развития API, о чем мы поговорим в разделе II.
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
##### 0. Правила — это всего лишь обобщения
|
||||
|
||||
Правила не действуют безусловно и не означают, что можно не думать головой. У каждого правила есть какая-то рациональная причина его существования. Если в вашей ситуации нет причин следовать правилу — значит, следовать ему не надо.
|
||||
Правила не действуют безусловно и не означают, что можно не думать головой. У каждого правила есть какая-то рациональная причина его существования. Если в вашей ситуации нет причин следовать правилу — значит, следовать ему не нужно.
|
||||
|
||||
Например, требование консистентности номенклатуры существует затем, чтобы разработчик тратил меньше времени на чтение документации; если вам _необходимо_, чтобы разработчик обязательно прочитал документацию по какому-то методу, вполне разумно сделать его сигнатуру нарочито неконсистентно.
|
||||
|
||||
@ -59,7 +59,7 @@ POST /v1/orders/statistics/aggregate
|
||||
|
||||
Поэтому _всегда_ указывайте, по какому конкретно стандарту вы отдаёте те или иные величины. Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе.
|
||||
|
||||
**Плохо**: `"date": "11/12/2020"` — стандартов записи дат существует огромное количество, плюс из этой записи невозможно даже понять, что здесь число, а что месяц.
|
||||
**Плохо**: `"date": "11/12/2020"` — существует огромное количество стандартов записи дат, плюс из этой записи невозможно даже понять, что здесь число, а что месяц.
|
||||
|
||||
**Хорошо**: `"iso_date": "2020-11-12"`.
|
||||
|
||||
@ -113,28 +113,28 @@ strpbrk (str1, str2)
|
||||
|
||||
Если поле называется `recipe` — мы ожидаем, что его значением является сущность типа `Recipe`. Если поле называется `recipe_id` — мы ожидаем, что его значением является идентификатор, который мы сможем найти в составе сущности `Recipe`.
|
||||
|
||||
То же касается и примитивных типов. Сущности-массивы должны именоваться во множественном числе или собирательными выражениями — `objects`, `children`; если это невозможно (термин неисчисляемый), следует добавить префикс или постфикс, не оставляющий сомнений.
|
||||
То же касается и примитивных типов. Сущности-массивы должны именоваться во множественном числе или собирательными выражениями — `objects`, `children`; если это невозможно (термин неисчисляем), следует добавить префикс или постфикс, не оставляющий сомнений.
|
||||
|
||||
**Плохо**: `GET /news` — неясно, будет ли получена какая-то конкретная новость или массив новостей.
|
||||
|
||||
**Хорошо**: `GET /news-list`.
|
||||
|
||||
Аналогично, если ожидается булево значение, то из названия это должно быть очевидно, т.е. именование должно описывать некоторое качественное состояние, например, `is_ready`, `open_now`.
|
||||
Аналогично, если ожидается булево значение, то это должно быть очевидно из названия, т.е. именование должно описывать некоторое качественное состояние, например, `is_ready`, `open_now`.
|
||||
|
||||
**Плохо**: `"task.status": true` — неочевидно, что статус бинарен, плюс такое API будет нерасширяемым.
|
||||
**Плохо**: `"task.status": true` — неочевидно, что статус бинарен, к тому же такое API будет нерасширяемым.
|
||||
|
||||
**Хорошо**: `"task.is_finished": true`.
|
||||
|
||||
Отдельно следует оговорить, что на разных платформах эти правила следует дополнить по-своему с учетом специфики first-class citizen-типов. Например, объекты типа `Date`, если таковые имеются, разумно индицировать с помощью, например, постфикса `_at` (`created_at`, `occurred_at` и т.д.) или `_date`.
|
||||
|
||||
Если наименование сущности само по себе является каким-либо термином, способным смутить разработчика, лучше добавить лишний префикс или постфикс, чтобы избежать непонимания.
|
||||
Если наименование сущности само по себе является каким-либо термином, способным смутить разработчика, лучше добавить лишний префикс или постфикс во избежание непонимания.
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
// Возвращает список встроенных функций кофе-машины
|
||||
GET /coffee-machines/{id}/functions
|
||||
```
|
||||
Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).
|
||||
Слово "functions" многозначное: оно может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).
|
||||
|
||||
**Хорошо**: `GET /v1/coffee-machines/{id}/builtin-functions-list`
|
||||
|
||||
@ -147,7 +147,7 @@ GET /coffee-machines/{id}/functions
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
// Находит первую позицию позицию строки `needle`
|
||||
// Находит первую позицию строки `needle`
|
||||
// внутри строки `haystack`
|
||||
strpos(haystack, needle)
|
||||
```
|
||||
@ -165,9 +165,9 @@ str_replace(needle, replace, haystack)
|
||||
|
||||
##### Используйте глобально уникальные идентификаторы
|
||||
|
||||
Хороший тон при разработке API — использовать для идентификаторов сущностей глобально уникальные строки, либо семантичные (например, "lungo" для видов напитков), либо случайные (например [UUID-4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random))). Это может чрезвычайно пригодиться, если вдруг придётся объединять данные из нескольких источников под одним идентификатором.
|
||||
Хорошим тоном при разработке API будет использование для идентификаторов сущностей глобально уникальных строк, либо семантичных (например, "lungo" для видов напитков), либо случайных (например [UUID-4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random))). Это может чрезвычайно пригодиться, если вдруг придётся объединять данные из нескольких источников под одним идентификатором.
|
||||
|
||||
Мы вообще склонны порекомендовать использовать идентификаторы в urn-подобном формате, т.е. `urn:order:<uuid>` (или просто `order:<uuid>`), это сильно помогает с отладкой legacy-систем, где по историческим причинам есть несколько разных идентификаторов для одной и той же сущности — тогда неймспейсы в urn помогут быстро понять, что это за идентификатор и нет ли здесь ошибки использования.
|
||||
Мы вообще склонны порекомендовать использование идентификаторов в urn-подобном формате, т.е. `urn:order:<uuid>` (или просто `order:<uuid>`), это сильно помогает с отладкой legacy-систем, где по историческим причинам есть несколько разных идентификаторов для одной и той же сущности, в таком случае неймспейсы в urn помогут быстро понять, что это за идентификатор и нет ли здесь ошибки использования.
|
||||
|
||||
Отдельное важное следствие: **не используйте инкрементальные номера как идентификаторы**. Помимо вышесказанного, это плохо ещё и тем, что ваши конкуренты легко смогут подсчитать, сколько у вас в системе каких сущностей и тем самым вычислить, например, точное количество заказов за каждый день наблюдений.
|
||||
|
||||
@ -175,7 +175,7 @@ str_replace(needle, replace, haystack)
|
||||
|
||||
##### Состояние системы должно быть понятно клиенту
|
||||
|
||||
Правило можно ещё сформулировать так: не заставляйте клиент гадать.
|
||||
Правило можно ещё сформулировать так: не заставляйте разработчика клиента гадать.
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
@ -423,7 +423,7 @@ X-Idempotency-Token: <см. следующий раздел>
|
||||
}
|
||||
```
|
||||
|
||||
Этот подход существенно сложнее в имплементации, но является единственным возможным вариантом реализации совместного редактирования, поскольку он явно отражает, что в действительности делал пользовать с представлением объекта. Имея данные в таком формате возможно организовать и оффлайн-редактирование, когда пользовательские изменения накапливаются и потом сервер автоматически разрешает конфликты, «перебазируя» изменения.
|
||||
Этот подход существенно сложнее в имплементации, но является единственным возможным вариантом реализации совместного редактирования, поскольку он явно отражает, что в действительности делал пользовать с представлением объекта. Имея данные в таком формате возможно организовать и оффлайн-редактирование, когда пользовательские изменения накапливаются и сервер впоследствии автоматически разрешает конфликты, «перебазируя» изменения.
|
||||
|
||||
##### Все операции должны быть идемпотентны
|
||||
|
||||
@ -461,7 +461,7 @@ PUT /v1/orders/drafts/{draft_id}
|
||||
Создание черновика заказа — необязывающая операция, которая не приводит ни к каким последствиям, поэтому допустимо создавать черновики без токена идемпотентности.
|
||||
Операция подтверждения заказа — уже естественным образом идемпотентна, для неё `draft_id` играет роль ключа идемпотентности.
|
||||
|
||||
Также стоит упомянуть, что добавление токенов идемпотентности к эндпойнтам, которые и так нативно идемпотентны, имеет определённый смысл, так как токен помогает различить две ситуации:
|
||||
Также стоит упомянуть, что добавление токенов идемпотентности к эндпойнтам, которые и так изначально идемпотентны, имеет определённый смысл, так как токен помогает различить две ситуации:
|
||||
* клиент не получил ответ из-за сетевых проблем и пытается повторить запрос;
|
||||
* клиент ошибся, пытаясь применить конфликтующие изменения.
|
||||
|
||||
@ -597,7 +597,7 @@ PATCH /v1/recipes
|
||||
* `error` — информация по ошибке для каждого изменения, если она возникла.
|
||||
|
||||
Не лишним будет также:
|
||||
* введение `sequence_id` в запросе, чтобы гарантировать порядок исполнения операций и соотнесение порядка статусов изменений в ответе с запросом;
|
||||
* ввести в запросе `sequence_id`, чтобы гарантировать порядок исполнения операций и соотнесение порядка статусов изменений в ответе с запросом;
|
||||
* предоставить отдельный эндпойнт `/changes-history`, чтобы клиент мог получить информацию о выполненных изменениях, если во время обработки запроса произошла сетевая ошибка или приложение перезагрузилось.
|
||||
|
||||
Неатомарные изменения нежелательны ещё и потому, что вносят неопределённость в понятие идемпотентности, даже если каждое вложенное изменение идемпотентно. Рассмотрим такой пример:
|
||||
@ -663,9 +663,9 @@ PATCH /v1/recipes
|
||||
|
||||
##### Указывайте политики кэширования
|
||||
|
||||
В клиент-серверном API, как правило, сеть и ресурс сервера не бесконечны, поэтому кэширование на клиенте результатов операции является стандартным действием.
|
||||
В клиент-серверном API, как правило, сеть и ресурс сервера не бесконечны, поэтому кэширование результатов операции на клиенте является стандартным действием.
|
||||
|
||||
Желательно в такой ситуации внести ясность; если не из сигнатур операций, то хотя бы из документации должно быть понятно, каким образом можно кэшировать результат.
|
||||
Желательно в такой ситуации внести ясность; каким образом можно кэшировать результат должно быть понятно, если не из сигнатур операций, то хотя бы из документации.
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
@ -721,18 +721,18 @@ GET /v1/records?limit=10&offset=100
|
||||
```
|
||||
На первый взгляд это самый что ни на есть стандартный способ организации пагинации в API. Однако зададим себе три вопроса.
|
||||
1. Каким образом клиент узнает о появлении новых записей в начале списка?
|
||||
Легко заметить, что клиент может только попытаться повторить первый запрос и сличить идентификаторы с запомненным началом списка. Но что делать, если добавленное количество записей превышает `limit`? Представим себе ситуацию:
|
||||
Легко заметить, что клиент может только попытаться повторить первый запрос и сверить идентификаторы с запомненным началом списка. Но что делать, если добавленное количество записей превышает `limit`? Представим себе ситуацию:
|
||||
* клиент обрабатывает записи в порядке поступления;
|
||||
* произошла какая-то проблема, и накопилось большое количество необработанных записей;
|
||||
* клиент запрашивает новые записи (`offset=0`), однако не находит на первой странице известных идентификаторов — новых записей накопилось больше, чем `limit`;
|
||||
* клиент вынужден продолжить перебирать записи (увеличивая `offset`), пока не доберётся до последней известной ему; всё это время клиент простаивает;
|
||||
* клиент вынужден продолжить перебирать записи (увеличивая `offset`) до тех пор, пока не доберётся до последней известной ему; всё это время клиент простаивает;
|
||||
* таким образом может сложиться ситуация, когда клиент вообще никогда не обработает всю очередь, т.к. будет занят беспорядочным линейным перебором.
|
||||
2. Что произойдёт, если при переборе списка одна из записей в уже перебранной части будет удалена?
|
||||
Произойдёт следующее: клиент пропустит одну запись и никогда не сможет об этом узнать.
|
||||
3. Какие параметры кэширования мы можем выставить на этот эндпойнт?
|
||||
Никакие: повторяя запрос с теми же `limit`-`offset`, мы каждый раз получаем новый набор записей.
|
||||
|
||||
**Хорошо**: в таких однонаправленных списках пагинация должна быть организована по тому ключу, порядок которых фиксирован. Например, вот так:
|
||||
**Хорошо**: в таких однонаправленных списках пагинация должна быть организована по тому ключу, порядок сортировки по которому фиксирован. Например, вот так:
|
||||
```
|
||||
// Возвращает указанный limit записей,
|
||||
// отсортированных по дате создания,
|
||||
@ -774,11 +774,11 @@ GET /v1/records?cursor=<значение курсора>
|
||||
|
||||
Вообще схему с курсором можно реализовать множеством способов (например, не разделять первый и последующие запросы данных), главное — выбрать какой-то один.
|
||||
|
||||
**NB**: в некоторых источниках такой подход, напротив, не рекомендуется, по следующей причине: пользователю невозможно показать список страниц и дать возможность выбрать произвольную. Здесь следует отметить, что:
|
||||
**NB**: в некоторых источниках такой подход, напротив, не рекомендуется по следующей причине: пользователю невозможно показать список страниц и дать возможность выбрать произвольную. Здесь следует отметить, что:
|
||||
* подобный кейс — список страниц и выбор страниц — существует только для пользовательских интерфейсов; представить себе API, в котором действительно требуется доступ к случайным страницам данных мы можем с очень большим трудом;
|
||||
* если же мы всё-таки говорим об API приложения, которое содержит элемент управления с постраничной навигацией, то наиболее правильный подход — подготавливать данные для этого элемента управления на стороне сервера, в т.ч. генерировать ссылки на страницы;
|
||||
* подход с курсором не означает, что `limit`/`offset` использовать нельзя — ничто не мешает сделать двойной интерфейс, который будет отвечать и на запросы вида `GET /items?cursor=…`, и на запросы вида `GET /items?offset=…&limit=…`;
|
||||
* наконец, если возникает необходимость предоставлять доступ к произвольной странице в пользовательском интерфейсе, то следует задать себе вопрос, какая проблема тем самым решается; вероятнее всего с помощью этой функциональности пользователь что-то ищет — определенный элемент списка или может быть позицию, на которой он закончил работу со списком в прошлый раз; возможно, следует предоставить для этих задач более удобные элементы управления, нежели перебор страниц.
|
||||
* наконец, если возникает необходимость предоставлять доступ к произвольной странице в пользовательском интерфейсе, то следует задать себе вопрос, какая проблема тем самым решается; вероятнее всего с помощью этой функциональности пользователь что-то ищет: определенный элемент списка или может быть позицию, на которой он закончил работу со списком в прошлый раз; возможно, для этих задач следует предоставить более удобные элементы управления, нежели перебор страниц.
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
@ -790,7 +790,7 @@ GET /records?sort_by=date_modified&sort_order=desc&limit=10&offset=100
|
||||
```
|
||||
Сортировка по дате модификации обычно означает, что данные могут меняться. Иными словами, между запросом первой порции данных и запросом второй порции данных какая-то запись может измениться; она просто пропадёт из перечисления, т.к. автоматически попадает на первую страницу. Клиент никогда не получит те записи, которые менялись во время перебора, и у него даже нет способа узнать о самом факте такого пропуска. Помимо этого отметим, что такое API нерасширяемо — невозможно добавить сортировку по двум или более полям.
|
||||
|
||||
**Хорошо**: в представленной постановке задача, вообще говоря, не решается. Список записей по дате изменения всегда будет непредсказуемо изменяться, поэтому необходимо изменить сам подход к формированию данных, одним из двух способов.
|
||||
**Хорошо**: в представленной постановке задача, собственно говоря, не решается. Список записей по дате изменения всегда будет непредсказуемо изменяться, поэтому необходимо изменить сам подход к формированию данных, одним из двух способов.
|
||||
|
||||
**Вариант 1**: фиксировать порядок в момент обработки запроса; т.е. сервер формирует полный список и сохраняет его в неизменяемом виде:
|
||||
|
||||
@ -810,7 +810,7 @@ POST /v1/record-views
|
||||
GET /v1/record-views/{id}?cursor={cursor}
|
||||
```
|
||||
|
||||
Т.к. созданное представление уже неизменяемо, доступ к нему можно организовать как угодно: через курсор, limit/offset, заголовок Range и т.д. Однако надо иметь в виду, что при переборе таких списков может получиться так, что порядок будет нарушен: записи, изменённые уже после генерации представления, будут находиться не на своих местах (либо быть неактуальны, если запись копируется целиком).
|
||||
Поскольку созданное представление уже неизменяемо, доступ к нему можно организовать как угодно: через курсор, limit/offset, заголовок Range и т.д. Однако надо иметь в виду, что при переборе таких списков порядок может быть нарушен: записи, изменённые уже после генерации представления, будут находиться не на своих местах (либо быть неактуальны, если запись копируется целиком).
|
||||
|
||||
**Вариант 2**: гарантировать строгий неизменяемый порядок записей, например, путём введения понятия события изменения записи:
|
||||
|
||||
@ -833,7 +833,7 @@ POST /v1/records/modified/list
|
||||
|
||||
##### Ошибки должны быть информативными
|
||||
|
||||
При написании кода разработчик неизбежно столкнётся с ошибками, в том числе самого примитивного толка — неправильный тип параметра или неверное значение. Чем понятнее ошибки, возвращаемые вашим API, тем меньше времени разработчик потратит на борьбу с ними, и тем приятнее работать с таким API.
|
||||
При написании кода разработчик неизбежно столкнётся с ошибками, в том числе самого примитивного толка: неправильный тип параметра или неверное значение. Чем понятнее ошибки, возвращаемые вашим API, тем меньше времени разработчик потратит на борьбу с ними, и тем приятнее работать с таким API.
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
@ -994,7 +994,7 @@ POST /search
|
||||
}
|
||||
```
|
||||
|
||||
Статусы `4xx` означают, что клиент допустил ошибку; однако в данном случае никакой ошибки сделано не было, ни пользователем, ни разработчиком: клиент же не может знать заранее, готовят здесь лунго или нет.
|
||||
Статусы `4xx` означают, что клиент допустил ошибку; однако в данном случае никакой ошибки сделано не было ни пользователем, ни разработчиком: клиент же не может знать заранее, готовят здесь лунго или нет.
|
||||
|
||||
**Хорошо**:
|
||||
```
|
||||
@ -1025,4 +1025,4 @@ POST /search
|
||||
|
||||
Следует отметить, что индикация, какие сообщения следует показать пользователю, а какие написаны для разработчика, должна, разумеется, быть явной конвенцией вашего API. В примере для этого используется префикс `localized_`.
|
||||
|
||||
И ещё одна вещь: все строки должны быть в кодировке UTF-8 и никакой другой.
|
||||
И ещё одна вещь: все строки должны быть в кодировке UTF-8 и никакой другой.
|
||||
|
Loading…
x
Reference in New Issue
Block a user