1
0
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:
Sergey Konstantinov 2022-09-11 23:46:07 +03:00
parent adbd991bcc
commit f366e10665

View File

@ -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 должен учитывать всё вышеперечисленное.