mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-02-22 18:42:09 +02:00
SDK - the beginning
This commit is contained in:
parent
a30f75c284
commit
6998c6a378
@ -2,14 +2,14 @@
|
||||
|
||||
Аббревиатура «SDK» («Software Development Kit»), как и многие из обсуждавшихся ранее терминов, не имеет конкретного значения. Считается, что SDK отличается от API тем, что помимо программных интерфейсов содержит и какое-то количество готовых инструментов для работы с ними. Определение это, конечно, лукавое, поскольку почти любая технология сегодня идёт в комплекте со своим набором инструментов.
|
||||
|
||||
Тем не менее, у термина SDK есть и более узкое значение, в котором он часто используется: это клиентская библиотека, которая предоставляет нативный интерфейс для работы с некоторым клиент-серверным API. Чаще всего речь идёт о библиотеках для мобильных платформ или веб-платформы, которые работают поверх нижележащего HTTP API сервиса.
|
||||
Тем не менее, у термина SDK есть и более узкое значение, в котором он часто используется: это клиентская библиотека, которая предоставляет нативный интерфейс для работы с некоторой нижележащей платформой (и в частности с клиент-серверным API). Чаще всего речь идёт о библиотеках для мобильных платформ или веб-платформы, которые работают поверх нижележащего HTTP API сервиса.
|
||||
|
||||
Среди подобных клиентских SDK особо выделяются те из них, которые предоставляют не только программные интерфейсы для работы с API, но также и готовые визуальные компоненты, которые разработчик может использовать. Классический пример такого SDK — это библиотеки карточных сервисов; в силу исключительной сложности самостоятельной реализации движка работы с картами (особенно векторными) вендоры API карт предоставляют и готовые библиотеки для работы с географическими сущностями, которые часто включают в себя и визуальные компоненты общего назначения — кнопки, метки, контекстные меню — которые могут применяться и совершенно самостоятельно вне контекста API (SDK) как такового.
|
||||
Среди подобных клиентских SDK особо выделяются те из них, которые предоставляют не только программные интерфейсы для работы с API, но также и готовые визуальные компоненты, которые разработчик может использовать. Классический пример такого SDK — это библиотеки карточных сервисов; в силу исключительной сложности самостоятельной реализации движка работы с картами (особенно векторными) вендоры API карт предоставляют и «обёртки» к HTTP API (например, поисковому), и готовые библиотеки для работы с географическими сущностями, которые часто включают в себя и визуальные компоненты общего назначения — кнопки, метки, контекстные меню — которые могут применяться и совершенно самостоятельно вне контекста API (SDK) как такового.
|
||||
|
||||
Настоящий раздел будет посвящён именно двум этим технологиям:
|
||||
* нативные клиентские «обёртки» поверх клиент-серверных API;
|
||||
* нативные клиентские библиотеки, предоставляющие не только «обёртки» над нижележащими API, но и визуальные компоненты, с которыми конечный пользователь может взаимодействовать напрямую.
|
||||
* клиентские «обёртки» поверх клиент-серверных API;
|
||||
* клиентские библиотеки, предоставляющие визуальные компоненты, с которыми конечный пользователь может взаимодействовать напрямую.
|
||||
|
||||
Во избежание нагромождения подобных оборотов мы будем называть первый тип библиотеки просто «SDK», а второй — «UI-библиотеки».
|
||||
|
||||
**NB**: вообще говоря, UI-библиотека может и не включать в себя никакой обёртки над клиент-серверным API. Этот случай интересует нас гораздо меньше (по причинам, которые мы изложим в следующих главах). Тем не менее, многие паттерны дизайна SDK, которые мы опишем далее, применимы и к «чистым» библиотекам. Аналогично, SDK может предоставлять доступ не к клиент-серверном или вообще сетевому API, а к какой-то нижележащей платформе; соображения, которые мы опишем в следующей главе, к таким SDK также применимы.
|
||||
**NB**: вообще говоря, UI-библиотека может как включать в себя обёртку над клиент-серверным API, так и предоставлять чистый API к графическому движку. В рамках этой книги мы будем говорить, в основном, о первом варианте, поскольку с точки зрения дизайна API второго вида существенно проще и не требует отдельной главы. Тем не менее, многие паттерны дизайна SDK, которые мы опишем далее, применимы и к «чистым» библиотекам без клиент-серверной составляющей.
|
@ -1,3 +1,208 @@
|
||||
### SDK: проблемы и решения
|
||||
|
||||
Первый вопрос, который мы должны себе задать при разработке SDK (напомним, так мы будем называть нативную клиентскую библиотеку, предоставляющую доступ к technology-agnostic клиент-серверному API) — почему вообще такое явление как SDK существует.
|
||||
Первый вопрос, который мы должны себе задать при разработке SDK (напомним, так мы будем называть нативную клиентскую библиотеку, предоставляющую доступ к technology-agnostic клиент-серверному API) — почему вообще такое явление как SDK существует. Иными словами, почему использование обёртки для фронтенд-разработчика является более удобным, нежели работа с нижележащим API напрямую.
|
||||
|
||||
Некоторые проблемы лежат на поверхности:
|
||||
1. Протоколы клиент-серверных API, как правило, разрабатываются так, что не зависят от конкретного языка программирования и, таким образом, без дополнительных действий полученные из API данные будут представлены в не самом удобном формате. Например в JSON нет типа данных «дата и время», и его приходится передавать в виде строки; или, скажем, поддержка (де)сериализации хэш-таблиц в протоколах общего назначения отсутствует.
|
||||
|
||||
2. Большинство языков программирования императивные (и чаще всего — объектно-ориентированные), в то время как большинство форматов данных — декларативные. Работать с сырыми данными, полученными из API, таким образом почти всегда неудобно с точки зрения написания кода.
|
||||
|
||||
3. Разные языки программирования предполагают разный стиль кодирования (кейсинг, организация неймспейсов и т.п.), в то время как концепция API не предполагает адаптацию форматирования под запрашивающую платформу.
|
||||
|
||||
4. Как правило, платформа/язык программирования диктуют свою парадигму работы с возникающими ошибками (в виде исключений и/или механизмов defer/panic), что опять же неприменимо в концепции универсального для всех клиентов сетевого API.
|
||||
|
||||
5. API идёт в комплекте с рекомендациями (машино- или человекочитаемыми) по организации перезапросов в случае недоступности эндпойнтов. Эту логику необходимо реализовать разработчику клиента, поскольку библиотеки работы с сетью её, как правило, не предоставляют (и в общем-то не могут этого делать для потенциально неидемпотентных запросов).
|
||||
|
||||
Эти проблемы, однако, являются тривиальными — в том смысле, что они не требуют изменять порядок с API (каждому вызову и каждому ответу в API однозначно соответствует какая-то конструкция на языке платформы, и нужно лишь описать правила построения такого соответствия), а только лишь адаптировать платформо-независимый формат API к правилам конкретного языка программирования. Помимо тривиальных проблем, при разработке SDK к клиент-серверному API мы сталкиваемся и с проблемами более высокого порядка:
|
||||
|
||||
1. В клиент-серверных API данные передаются только по значению; чтобы сослаться на какую-то сущность, необходимо использовать какие-то внешние идентификаторы. Например, если у нас есть два набора сущностей — рецепты и предложения кофе — то нам необходимо будет построить карту рецептов по id, чтобы понять, на какой рецепт ссылается какое предложение:
|
||||
|
||||
```
|
||||
// Запрашиваем информацию о рецептах
|
||||
// лунго и латте
|
||||
const recipes = await api
|
||||
.getRecipes(['lungo', 'latte']);
|
||||
// Строим карту, позволяющую обратиться
|
||||
// к данным о рецепте по его id
|
||||
const recipeMap = new Map();
|
||||
recipes.forEach((recipe) => {
|
||||
recipeMap.set(recipe.id, recipe);
|
||||
});
|
||||
// Запрашиваем предложения
|
||||
// лунго и латте
|
||||
const offers = await api.search({
|
||||
recipes: ['lungo', 'latte'],
|
||||
location
|
||||
});
|
||||
// Для того, чтобы показать предложения
|
||||
// пользователю, нужно из каждого
|
||||
// предложения извлечь id рецепта,
|
||||
// и уже по id найти описание
|
||||
promptUser(
|
||||
'Найденные предложения',
|
||||
offers.map((offer) => {
|
||||
const recipe = recipeMap
|
||||
.get(offer.recipe_id);
|
||||
return recipe.name;
|
||||
}));
|
||||
```
|
||||
|
||||
Указанный код мог бы быть вдвое короче, если бы мы сразу получали из метода `api.search` предложения с заполненной ссылкой на рецепт:
|
||||
|
||||
```
|
||||
// Запрашиваем информацию о рецептах
|
||||
// лунго и латте
|
||||
const recipes = await api
|
||||
.getRecipes(['lungo', 'latte']);
|
||||
// Запрашиваем предложения
|
||||
// лунго и латте
|
||||
const offers = await api.search({
|
||||
// Передаём не идентификаторы
|
||||
// рецептов, а ссылки на объекты,
|
||||
// описывающие рецепты
|
||||
recipes,
|
||||
location
|
||||
});
|
||||
|
||||
promptUser(
|
||||
'Найденные предложения',
|
||||
offers.map((offer) => {
|
||||
return offer.recipe.name;
|
||||
}));
|
||||
```
|
||||
|
||||
2. Клиент-серверные API, как правило, стараются декомпозировать так, чтобы одному запросу соответствовал один тип возвращаемых данных. Даже если эндпойнт композитный (т.е. позволяет при запросе с помощью параметров указать, какие из дополнительных данных необходимо вернуть), это всё ещё ответственность разработчика этими параметрами воспользоваться. Код из примера выше мог бы быть ещё короче, если бы SDK взял на себя инициализацию всех нужных связанных объектов:
|
||||
|
||||
```
|
||||
// Запрашиваем предложения
|
||||
// лунго и латте
|
||||
const offers = await api.search({
|
||||
recipes: ['lungo', 'latte'],
|
||||
location
|
||||
});
|
||||
|
||||
promptUser(
|
||||
'Найденные предложения',
|
||||
offers.map((offer) => {
|
||||
// SDK сам обратился к эндпойнту
|
||||
// `getRecipes` и получил данные
|
||||
// по лунго и латте
|
||||
return offer.recipe.name;
|
||||
}));
|
||||
```
|
||||
|
||||
При этом SDK может также заполнять программные кэши сущностей (если мы не полагаемся на кэширование на уровне протокола) и/или позволять «лениво» инициализировать объекты:
|
||||
|
||||
```
|
||||
promptUser(
|
||||
'Найденные предложения',
|
||||
offers.map((offer) => {
|
||||
// SDK обратится за данными
|
||||
// о рецепте, только если
|
||||
// они действительно нужны
|
||||
const recipe = await offer
|
||||
.getRecipe()
|
||||
return recipe.name;
|
||||
}));
|
||||
```
|
||||
|
||||
**NB**: Как видно из примера выше, эта функциональность («ленивая инициализация») требует очень внимательного подхода к имплементации со стороны разработчиков SDK. Если SDK не включает в себя агрегацию запросов и кэширование результатов, то пример кода выше запросит рецепт в цикле на каждое предложение, то есть выполнит огромное количество лишней работы. Мы можем описать эту тонкость в документации, конечно, но вновь закон больших чисел работает против нас: среди множества разработчиков обязательно найдутся те, кто не прочитает документацию или просто не задумается о том, что он запрашивает с сервера одни и те же данные в цикле.
|
||||
|
||||
3. Получение обратных вызовов в клиент-серверном API, даже если это дуплексный канал, с точки зрения клиента выглядит крайне неудобным в разработке, поскольку вновь требует наличия карт объектов. Даже если в API реализована push-модель, код выходит чрезвычайно громоздким:
|
||||
|
||||
```
|
||||
// Получаем текущие заказы
|
||||
const orders = await api
|
||||
.getOngoingOrders();
|
||||
// Строим карту заказов
|
||||
const orderMap = new Map();
|
||||
orders.forEach((order) => {
|
||||
orderMap.set(order.id, order);
|
||||
});
|
||||
// Подписываемся на события
|
||||
// изменения состояния заказов
|
||||
api.subscribe(
|
||||
'order_state_change',
|
||||
(event) => {
|
||||
const order = orderMap
|
||||
.get(event.order_id);
|
||||
// Выполняем какие-то
|
||||
// действия с заказом,
|
||||
// например, обновляем UI
|
||||
// приложения
|
||||
UI.update(order);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Если же API требует поллинга изменений состояний объектов, то разработчику придётся ещё где-то реализовать периодический опрос эндпойнта со списком изменений, и ещё следить за тем, чтобы не перегружать сервер запросами.
|
||||
|
||||
Кроме того, обратите внимание, что в вышеприведённом фрагменте кода [разработчиком приложения] допущены множественные ошибки:
|
||||
* сначала получается список заказов, а затем происходит подписывание на их изменения; если между двумя этими вызовами какой-то из заказов изменился, приложение об этом не узнает;
|
||||
* если пришло событие изменения какого-то неизвестного приложению заказа (который, например, был создан с другого устройства или в другом потоке исполнения), поиск в карте заказов вернёт пустой результат.
|
||||
|
||||
И вновь мы приходим к тому, что недостаточно продуманный SDK приводит к ошибкам в работе использующих его приложений. Разработчику было бы намного удобнее, если бы объект заказа позволял подписаться на свои события, не задумываясь о том, как эта подписка технически работает и как не пропустить события:
|
||||
|
||||
```
|
||||
const order = await api.
|
||||
createOrder(…);
|
||||
// Нет нужды подписываться
|
||||
// на *все* события и потом
|
||||
// фильтровать их по id
|
||||
order.subscribe(
|
||||
'state_change',
|
||||
(event) => { … }
|
||||
);
|
||||
```
|
||||
|
||||
**NB**: код выше предполагает, что объект `order` изменяется консистентным образом: даже если между вызовами `createOrder` и `subscribe` состояние заказа успело измениться на сервере, обработчик события `state_change` это изменение получит. Как это организовать технически — как раз забота разработчика SDK.
|
||||
|
||||
4. Восстановление после ошибок в бизнес-логике, как правило, достаточно сложная операция, которую сложно описать в машиночитаемом виде. Разработчику клиента необходимо самому продумать эти сценарии.
|
||||
|
||||
```
|
||||
// Получаем предложения
|
||||
const offers = await api
|
||||
.search(…);
|
||||
// Пользователь выбирает
|
||||
// подходящее предложение
|
||||
const selectedOffer =
|
||||
await promptUser(offers);
|
||||
let order;
|
||||
let offer = selectedOffer;
|
||||
let numberOfTries = 0;
|
||||
do {
|
||||
// Пытаемся создать заказ
|
||||
try {
|
||||
numberOfTries++;
|
||||
order = await api
|
||||
.createOrder(offer, …);
|
||||
} catch (e) {
|
||||
// Если количество попыток
|
||||
// пересоздания заказа превысило
|
||||
// какое-то разумное значение
|
||||
// следует бросить попытки
|
||||
// восстановиться
|
||||
if (numberOfTries > TRY_LIMIT) {
|
||||
throw new NoRetriesLeftError();
|
||||
}
|
||||
// Если произошла ошибка
|
||||
// «предложение устарело»
|
||||
if (e.type ==
|
||||
api.OfferExpiredError) {
|
||||
// если попытки ещё остались,
|
||||
// пытаемся получить новое
|
||||
// предложение
|
||||
offer = await api
|
||||
.renewOffer(offer);
|
||||
} else {
|
||||
// Обработка других видов
|
||||
// ошибок
|
||||
…
|
||||
}
|
||||
}
|
||||
} while (!order);
|
||||
```
|
||||
|
||||
Как мы видим, простая операция «попробовать продлить предложение» выливается в громоздкий код, в котором легко ошибиться, и, что ещё важнее, который совершенно не нужен разработчику приложения. Было бы гораздо проще, если бы этой ошибки *вовсе не было в SDK*, т.е. попытки обновления и перезапросы выполнялись бы автоматически.
|
||||
|
||||
Аналогичные ситуации возникают и в случае нестрого-консистентных API или оптимистичного управления параллелизмом — и вообще в любом API, в котором фон ошибок является ожидаемым (что в в случае распределённых клиент-серверных API является нормой жизни). Передача версии ресурса и/или последних известных идентификаторов в эндпойнты с политикой read-your-writes — техническая необходимость, вызванная стремлением вендора API удешевить эксплуатацию и увеличить пропускную способность. Для разработчика приложения написание кода, имплементирующего эти политики — попросту напрасная трата времени.
|
@ -1 +1,2 @@
|
||||
### Кодогенерация
|
||||
### Паттерн «Кодогенерация»
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user