mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-01-05 10:20:22 +02:00
refactoring continued
This commit is contained in:
parent
adbd991bcc
commit
f366e10665
@ -6,7 +6,7 @@
|
||||
|
||||
##### 0. Правила не должны применяться бездумно
|
||||
|
||||
Правила — это просто крастно сформулированное обобщение опыта. Они не действуют безусловно и не означают, что можно не думать головой. У каждого правила есть какая-то рациональная причина его существования. Если в вашей ситуации нет причин следовать правилу — значит, следовать ему не нужно.
|
||||
Правило — это просто кратко сформулированное обобщение опыта. Они не действуют безусловно и не означают, что можно не думать головой. У каждого правила есть какая-то рациональная причина его существования. Если в вашей ситуации нет причин следовать правилу — значит, следовать ему не нужно.
|
||||
|
||||
Например, требование консистентности номенклатуры существует затем, чтобы разработчик тратил меньше времени на чтение документации; если вам _необходимо_, чтобы разработчик обязательно прочитал документацию по какому-то методу, вполне разумно сделать его сигнатуру нарочито неконсистентно.
|
||||
|
||||
@ -210,7 +210,7 @@ if (Type(order.contactless_delivery) == 'Boolean' &&
|
||||
|
||||
Эта практика ведёт к усложнению кода, который пишут разработчики, и в этом коде легко допустить ошибку, которая по сути меняет значение поля на противоположное. То же самое произойдёт, если для индикации отсутствия значения поля использовать специальное значение типа `null` или `-1`.
|
||||
|
||||
**NB**. Это замечание не распространяется на те случае, когда платформа и протокол однозначно и без всяких дополнительных абстракций поддерживают такие специальные значения для сброса значения поля в значение по умолчанию. Однако полная и консистентная поддержка частичных операций со сбросом значений полей практически нигде не имплементирована. Пожалуй, единственный пример такого API из имеющих широкое распространение сегодня — SQL: в языке есть и концепция `NULL`, и значения полей по умолчанию, и поддержка конструкций вида `UPDATE … SET field = DEFAULT` (в большинстве диалектов). Хотя работа с таким протоколом всё ещё затруднена (например, во многих диалектах нет простого способа получить обратно значение по умолчанию, которое выставил `UPDATE`), частичные обновления в нём имплементированы достаточно хорошо, чтобы использовать их без дополнительных уточнений.
|
||||
**NB**. Это замечание не распространяется на те случае, когда платформа и протокол однозначно и без всяких дополнительных абстракций поддерживают такие специальные значения для сброса значения поля в значение по умолчанию. Однако полная и консистентная поддержка частичных операций со сбросом значений полей практически нигде не имплементирована. Пожалуй, единственный пример такого API из имеющих широкое распространение сегодня — SQL: в языке есть и концепция `NULL`, и значения полей по умолчанию, и поддержка операций вида `UPDATE … SET field = DEFAULT` (в большинстве диалектов). Хотя работа с таким протоколом всё ещё затруднена (например, во многих диалектах нет простого способа получить обратно значение по умолчанию, которое выставил `UPDATE … DEFAULT`), логика работы с умолчаниями в SQL имплементирована достаточно хорошо, чтобы использовать её как есть.
|
||||
|
||||
Если же протоколом явная работа со значениями по умолчанию не предусмотрена, универсальное правило — все новые необязательные булевы флаги должны иметь значение по умолчанию false.
|
||||
|
||||
@ -904,9 +904,73 @@ PATCH /v1/recipes
|
||||
|
||||
**Во-вторых**, чрезвычайно самонадеянно (и опасно) считать, что вы разбираетесь в вопросах безопасности. Новые вектора атаки появляются каждый день, и быть в курсе всех актуальных проблем — это само по себе работа на полный рабочий день. Если же вы полный рабочий день занимаетесь чем-то другим, спроектированная вами система защиты наверняка будет содержать уязвимости, о которых вы просто никогда не слышали — например, ваш алгоритм проверки паролей может быть подвержен [атаке по времени](https://en.wikipedia.org/wiki/Timing_attack), а веб-сервер — [атаке с разделением запросов](https://capec.mitre.org/data/definitions/105.html).
|
||||
|
||||
##### Декларируйте технические ограничения явно
|
||||
|
||||
У любого поля в вашем API есть ограничения на допустимые значения: размеры текстовых полей, объём прикладываемых документов в мегабайтах, разрешённые диапазоны цифровых значений. Часто разработчики API пренебрегают указанием этих лимитов — либо потому, что считают их очевидными, либо потому, что попросту не знают их сами. Это, разумеется, один большой антипаттерн: незнание пределов использования системы автоматически означает, что код партнёров может в любой момент перестать работать по не зависящим от них причинам.
|
||||
|
||||
Поэтому, во-первых, указывайте границы допустимых значений для всех без исключения полей в API, и, во-вторых, если эти границы нарушены, генерируйте машиночитаемую ошибку с описанием, какое ограничение на какое поле было нарушено.
|
||||
|
||||
##### Считайте трафик
|
||||
|
||||
В современном мире
|
||||
В современном мире такой ресурс, как объём пропущенного трафика, считать уже почти не принято — считается, что Интернет всюду практически безлимитен. Однако он всё-таки не абсолютно безлимитен: всегда можно спроектировать систему так, что объём трафика окажется некомфортным даже и для современных сетей, и API способно выступать мультипликатором ошибок и в этом вопросе.
|
||||
|
||||
Три основные причины раздувания объёма трафика достаточно очевидны:
|
||||
* не предусмотрен постраничный перебор данных;
|
||||
* не предусмотрены ограничения на размер значений полей и/или передаются большие бинарные данные (графика, аудио, видео и т.д.);
|
||||
* клиент слишком часто запрашивает данные и/или слишком мало их кэширует.
|
||||
|
||||
Если первые две проблемы решаются чисто техническими средствами (см. соответствующие разделы), то вторая проблема скорее логическая: каким образом разумно организовать канал обновления состояния клиента так, чтобы найти баланс между отзывчивостью системы и затраченными на эту отзывчивость ресурсами. Здесь мы можем дать несколько рекомендаций:
|
||||
* не злоупотребляйте асинхронными интерфейсами; с одной стороны, они позволяют избежать многих технических проблем с производительностью API, что, в свою очередь, позволяет поддерживать обратную совместимость (если метод изначально асинхронный, то можно без проблем увеличивать время обработки и менять модель консистентности данных); но, с другой стороны, количество генерируемых клиентами запросов становится трудно предсказуемым;
|
||||
* объявляйте явную политику перезапросов (например, посредством заголовка `Retry-After`); да, какие-то клиенты будут её игнорировать, т.к. разработчики поленятся её имплементировать, но какие-то не будут (особенно если вы сами предоставляете SDK);
|
||||
* если вы ожидаете значительного количества асинхронных операций в API, изначально дайте разработчику выбор между poll (клиент самостоятельно производит новые запросы к API чтобы проверить, не изменился ли статус асинхронной операций) и push (сервер уведомляет клиентов об изменениях статусов посредством отправки специального запроса, например, через webhook-и или server push-механизмы) моделью;
|
||||
* если в рамках одной сущности необходимо предоставлять как «лёгкие» (скажем, название и описание рецепта), так и «тяжёлые» данные (скажем, промо-фотография напитка, которая легко может по объёмы превышать текстовые поля в сотни раз), лучше разделить эндпойнты и отдавать только ссылку для доступа к «тяжёлым» данным (в нашем случае, ссылку на изображение) — это как минимум позволит задавать различные политики кэширования для разных данных.
|
||||
|
||||
Неплохим упражнением здесь будет промоделировать типовой жизненный цикл основной функциональности приложения (например, выполнение одного заказа) и подсчитать общее количество запросов и объём трафика на один цикл.
|
||||
|
||||
##### Указывайте политики кэширования
|
||||
|
||||
В клиент-серверном API, как правило, сеть и ресурс сервера не бесконечны, поэтому кэширование результатов операции на клиенте является стандартным действием.
|
||||
|
||||
Желательно в такой ситуации внести ясность; каким образом можно кэшировать результат должно быть понятно, если не из сигнатур операций, то хотя бы из документации.
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
// Возвращает цену лунго в кафе,
|
||||
// ближайшем к указанной точке
|
||||
GET /v1/price?recipe=lungo
|
||||
&longitude={longitude}&latitude={latitude}
|
||||
→
|
||||
{ "currency_code", "price" }
|
||||
```
|
||||
Возникает два вопроса:
|
||||
* в течение какого времени эта цена действительна?
|
||||
* на каком расстоянии от указанной точки цена всё ещё действительна?
|
||||
|
||||
**Хорошо**:
|
||||
Для указания времени жизни кэша можно пользоваться стандартными средствами протокола, например, заголовком `Cache-Control`. В ситуации, когда кэш нужен и по временной, и по пространственной координате следует поступить примерно так:
|
||||
```
|
||||
// Возвращает предложение: за какую сумму
|
||||
// наш сервис готов приготовить лунго
|
||||
GET /v1/price?recipe=lungo
|
||||
&longitude={longitude}&latitude={latitude}
|
||||
→
|
||||
{
|
||||
"offer": {
|
||||
"id",
|
||||
"currency_code",
|
||||
"price",
|
||||
"conditions": {
|
||||
// До какого времени валидно предложение
|
||||
"valid_until",
|
||||
// Где валидно предложение:
|
||||
// * город
|
||||
// * географический объект
|
||||
// * …
|
||||
"valid_within"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Избегайте неявных частичных обновлений
|
||||
|
||||
@ -931,19 +995,7 @@ PATCH /v1/orders/123
|
||||
```
|
||||
— такой подход часто практикуют для того, чтобы уменьшить объёмы запросов и ответов, плюс это позволяет дёшево реализовать совместное редактирование. Оба этих преимущества на самом деле являются мнимыми.
|
||||
|
||||
**Во-первых**, экономия объёма ответа в современных условиях требуется редко. Максимальные размеры сетевых пакетов (MTU, Maximum Transmission Unit) в настоящее время составляют более килобайта; пытаться экономить на размере ответа, пока он не превышает килобайт — попросту бессмысленная трата времени.
|
||||
|
||||
Перерасход трафика возникает, если:
|
||||
|
||||
* не предусмотрен постраничный перебор данных;
|
||||
* не предусмотрены ограничения на размер значений полей;
|
||||
* передаются бинарные данные (графика, аудио, видео и т.д.).
|
||||
|
||||
Во всех трёх случаях передача части полей в лучшем случае замаскирует проблему, но не решит. Более оправдан следующий подход:
|
||||
|
||||
* для «тяжёлых» данных сделать отдельные эндпойнты;
|
||||
* ввести пагинацию и лимитирование значений полей;
|
||||
* на всём остальном не пытаться экономить.
|
||||
**Во-первых**, экономия объёма ответа в таком формате бессмысленна. Максимальные размеры сетевых пакетов (MTU, Maximum Transmission Unit) в настоящее время составляют более килобайта; пытаться экономить на размере ответа, пока он не превышает килобайт — попросту бессмысленная трата времени. Реальные причины слишком высокого расхода трафика мы описали выше.
|
||||
|
||||
**Во-вторых**, экономия размера ответа выйдет боком как раз при совместном редактировании: один клиент не будет видеть, какие изменения внёс другой. Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа не оказывает значительного влияния на производительность) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.
|
||||
|
||||
@ -1002,51 +1054,6 @@ X-Idempotency-Token: <см. следующий раздел>
|
||||
|
||||
Этот подход существенно сложнее в имплементации, но является единственным возможным вариантом реализации совместного редактирования, поскольку он явно отражает, что в действительности делал пользовать с представлением объекта. Имея данные в таком формате возможно организовать и оффлайн-редактирование, когда пользовательские изменения накапливаются и сервер впоследствии автоматически разрешает конфликты, «перебазируя» изменения.
|
||||
|
||||
##### Указывайте политики кэширования
|
||||
|
||||
В клиент-серверном API, как правило, сеть и ресурс сервера не бесконечны, поэтому кэширование результатов операции на клиенте является стандартным действием.
|
||||
|
||||
Желательно в такой ситуации внести ясность; каким образом можно кэшировать результат должно быть понятно, если не из сигнатур операций, то хотя бы из документации.
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
// Возвращает цену лунго в кафе,
|
||||
// ближайшем к указанной точке
|
||||
GET /v1/price?recipe=lungo
|
||||
&longitude={longitude}&latitude={latitude}
|
||||
→
|
||||
{ "currency_code", "price" }
|
||||
```
|
||||
Возникает два вопроса:
|
||||
* в течение какого времени эта цена действительна?
|
||||
* на каком расстоянии от указанной точки цена всё ещё действительна?
|
||||
|
||||
**Хорошо**:
|
||||
Для указания времени жизни кэша можно пользоваться стандартными средствами протокола, например, заголовком `Cache-Control`. В ситуации, когда кэш нужен и по временной, и по пространственной координате следует поступить примерно так:
|
||||
```
|
||||
// Возвращает предложение: за какую сумму
|
||||
// наш сервис готов приготовить лунго
|
||||
GET /v1/price?recipe=lungo
|
||||
&longitude={longitude}&latitude={latitude}
|
||||
→
|
||||
{
|
||||
"offer": {
|
||||
"id",
|
||||
"currency_code",
|
||||
"price",
|
||||
"conditions": {
|
||||
// До какого времени валидно предложение
|
||||
"valid_until",
|
||||
// Где валидно предложение:
|
||||
// * город
|
||||
// * географический объект
|
||||
// * …
|
||||
"valid_within"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Продуктовое качество API
|
||||
|
||||
Помимо технологических ограничений, любой реальный API скоро столкнётся и с несовершенством окружающей действительности. Конечно, мы все хотели бы жить в мире розовых единорогов, свободном от накопления legacy, злоумышленников, национальных конфликтов и происков конкурентов. Но, к сожалению или к счастью, живём мы в реальном мире, в котором хороший API должен учитывать всё вышеперечисленное.
|
||||
|
Loading…
Reference in New Issue
Block a user