Дизайн данных (часть 6). Матчасть: API.

В этой статье речь снова пойдёт об обмене информацией между компонентами системы. Однако на этот раз мы коснёмся более глубоких тем: рассмотрим, как создаётся философия API и даже пощупаем реальные примеры запросов.

8-10 минут на прочтение
Дизайн данных (часть 6). Матчасть: API.

Это шестой, предпоследний, пост цикла. И он же третий про матчасть дизайна данных.

В этой статье (как и в третьей, «Меняемся?») речь снова пойдёт об обмене информацией между компонентами системы. Однако на этот раз мы коснёмся более глубоких тем: рассмотрим, как создаётся философия API и даже пощупаем реальные примеры запросов.

Предупреждаю, дальше вас ожидает полный хардкор — просьба убрать от мониторов несовершеннолетних детей, иллюстраторов и прочих убеждённых юайщиков.

Погнали.

Виды API

Как я уже упоминал, видов API огромное множество, ведь API — это лишь общее название всех программных интерфейсов. Например, есть API, позволяющий умному холодильнику коннектиться к Амазону и заказывать продукты, если они вдруг у вас кончились. При том у этого же холодильника есть ПО, которое с помощью уже другого API получает данные с внутренних сканеров (чтобы понять, каких продуктов не хватает). А также еще один API для доступа к микрофону, чтобы вы могли велеть своему холодильнику купить молока (и там же рядом API, передающий ваши слова на сервер для обработки). И еще один, чтобы синхронизироваться с вашими умными весами, и на основании их данных составлять примерное меню. И так до бесконечности, насколько хватило фантазии у дизайнеров упомянутого холодильника.

Суть в том, что в примере выше есть «физические» API, которые дают возможность взаимодействовать с различным «железом» (типа сканеров или микрофона), а есть другие, которые позволяют обмениваться данными с различными внешними источниками (вроде серверов Амазона). Мы будем рассматривать исключительно вторые, так как именно с ними чаще всего придётся работать продуктовому дизайнеру.

Немножко лирики

Догадливый читатель уже смекнул, что без понимания того, как эти API работают (и есть ли они вообще), невозможно спроектировать полноценную функциональность нашего холодильника (это я уже молчу о пользовательском опыте).

Ну и разумеется, сей тезис касается не только физических предметов: большинству дизайнеров даже не придёт в голову изучить вопрос внешних интеграций перед тем, как приступить к созданию нового цифрового продукта. Редкий проектировщик гуглит сервисы пуш-уведомлений (например), чтобы понять, какие у них есть ограничения.

Подавляющее большинство создаваемых нами сайтов и приложений обменивается информацией с серверами. Сейчас это просто данность, и дизайнер не имеет права игнорировать этот аспект своей работы. Хотя бы просто потому, что именно он (а не программный архитектор или, тем более, рядовой программист) знает создаваемый продукт лучше всех. Дизайнер может не разбираться в деталях, постоянно обращаться за помощью к технарям — но он должен понимать, как данные будут жить внутри его приложения, как они будут обмениваться с сервером и какие потенциальные риски могут при этом образоваться.

В одной статье я не научу вас создавать пуленепробиваемый API — но это и не нужно. Для начала хватит самого базового погружения. А дальше всё сделает опыт.

Философия API

На самом деле, термин «Философия API» на проектном уровне употребляется довольно редко. Однако, по моему субъективному мнению, именно он как нельзя лучше передаёт суть подхода. По факту, упомянутая философия — это набор основополагающих правил, по которым создаётся и развивается конкретный API.

Философия API описывает то, как именно будут обмениваться между собой стороны (например, мобильное приложение и сервер). Это некие верхнеуровневые правила, которые задают тон всему остальному. Без них развивать и поддерживать API довольно сложно: любой программист, добавляя тот или иной метод, руководствуется собственными представлениями о валидации (например). Единая философия позволяет унифицировать запросы/ответы, не ограничивая возможностей разработки.

Чем разжёвывать дальше, лучше приведу фрагмент документации одного из моих последних проектов:

Общие принципы

Описываемый в настоящем документе API является общим для всех сторон, если иное не указано отдельно. Это сделано в целях унификации обмена данными и упрощения развития API впоследствии.

Обмен информацией между Сторонами осуществляется только по защищенному SSL протоколу.

Именование методов/параметров

При обмене данными действует правило именования параметров: имена всех служебных параметров, которые касаются непосредственно сторон (параметры авторизации, названия запросов и т.д.), имеют префикс dt_, все прочие параметры его не имеют.

Все названия методов и параметров начинаются с маленькой буквы.

В названиях методов, состоящих из нескольких слов, определяющих сущность и действие, в качестве разделителя используется точка (например, user.registration).

В документации вложенность элементов (свойства сущностей) также разделяется точкой (например, user.id).

В названиях параметров, состоящих из нескольких слов, используется «CamelCase»: например, secretKey (исключением являются аббревиатуры, состоящие из заглавных букв — например, SMS). Это не относится только к префиксу dt_.

Форматы значений

В значениях всех параметров:

ПараметрТипФорматПримерПримечание
Номер телефонаintegerXXXXXXXXXXX7955444778811 цифр международном формате без +
Дата и времяstringYYYY-MM-DDThh:mm:ss±hh2005-08-09T18:31:42-03ISO Date
Булевы значенияstringtrueВсегда true|false

Допустима передача пустых массивов в случае возвращения функцией списка, состоящего из 0 сущностей. Во всех остальных случаях следует возвращать false.

Не допускаются значения null, undefined и подобные.

Формат сообщений

Все запросы и ответы передаются в кодировке UTF-8 методом POST в формате JSON.

Сервер всегда отвечает HTTP-кодом 200, даже в случае попытки неавторизованного доступа (в этом случае сервер возвращает код ошибки из словаря ошибок).

Обязательные параметры

В запросах и ответах есть ряд обязательных параметров, без которых сообщение не обрабатывается.

Запрос

Во всех запросах обязательно должны присутствовать параметры:

ПолеТипПримерОписание
dt_requeststringuser.logoutПараметр, отвечающий за тип запроса. В рамках одной области типов может быть несколько.
Ответ

Во всех ответах обязательно должны присутствовать параметры:

ПолеТипПримерОписание
dt_statusstringsuccessСтатус выполнения запроса, допустимые значения success|error.
dt_errorCodearray[1800]Является обязательным только в случае, если status==error. Принимает значения соответствующего кода ошибки из словаря ошибок.

Обычно я дополняю философию еще несколькими примерами запросов, чтобы архитектор и разработчики точно представляли, о чём речь. И в самом конце статьи я как раз представлю один такой.

Назад к тюленям

Это была теория. Давайте теперь вернёмся в нашему восхитительному приложению, позволяющему отслеживать миграцию тюленей на берегу Охотского моря.

В одной из предыдущих статей мы создали графическую схему запросов нашего выдуманного приложения. Если кто запамятовал, то вот она:

Учитывая, что у нас здесь только схема запросов, а не ответов, последние будем додумывать на ходу. Благо, во втором посте цикла мы создали диаграмму со структурой данных нашего приложения:

Теперь, руководствуясь описанной выше философией, проименуем сущности, их свойства и методы API.

Сущности

Итак, наши сущности, их всего четыре:

  • пользователь — user;
  • мобильное приложение — app;
  • точка — point;
  • список точек — pointList.

Свойства

Тут посложнее. Будем отталкиваться от сущностей:

Пользователь

У пользователя пять свойств:

  • номер телефона — phone;
  • внутренний идентификатор — id (не участвует в обмене данными);
  • глобальный идентификатор — guid;
  • секретный ключ — secretKey;
  • дата и время регистрации — regTime.

Мобильное приложение

Здесь всего три свойства:

  • версия — version;
  • идентификатор устройства — deviceId;
  • платформа — platform.

Точка

У точки, как и у пользователя, пять свойств, но три из них сложные:

  • внутренний идентификатор — id (не участвует в обмене данными);
  • дата и время — dateTime;
  • координаты — coords, которые состоят из двух параметров:
    • долгота — long;
    • широта — lat;
  • миниатюра изображения — thumb, состоящая также из двух параметров:
    • ссылка — url;
    • хэш — hash;
  • полноразмерное изображение — picture, аналогично предыдущему свойству:
    • ссылка — url;
    • хэш — hash.

Те, кто помнит, как мы проектировали кэширование, понимает, зачем нужны такие сложности с последними двумя свойствами.

Список точек

У списка всего два свойства, используемых для адаптации под возможности конкретного девайса:

  • отступ — offset;
  • область видимости — scope.

Методы API

Методов у нас всего три:

  • регистрация — registration;
  • подтверждение номера телефона — confirm;
  • получение списка точек — getPoints.

Единая таблица свойств

Теперь нам надо определиться с типом данных каждого свойства (я рассказывал про типы в четвёртом посте цикла про дизайн данных). Здесь всё просто: выбираем наиболее подходящий тип и не забываем поглядывать в философию нашего API:

Название параметраТип данныхПример
user.phoneinteger79554447788
user.idinteger3
user.guidstringc0deaca4-db8a-4178-8863-eaf2494833b9
user.secretKeystring4af0744c305991c098753117a6a19690
user.regTimestring2019-06-09T16:28:19-03
app.versionstring1.0.1
app.deviceIdstring2fc4b5912826ad1
app.platformstringiOS
point.idinteger32
point.dateTimestring2019-09-12T19:05:12-03
point.coords.longinteger59.5194488
point.coords.latinteger150.4879161
point.thumb.urlstring/thumbs/uat12ia.jpg
point.thumb.hashstring05991c09f0744a4c38756903117a6a19
point.picture.urlstring/picture/hrt6q7.jpg
point.picture.hashstring305991c094af0744c876a1969053117a
pointList.offsetinteger20
pointList.scopeinteger60

В общем-то, такой таблицы нам достаточно для того, чтобы приступить к описанию самого обмена данными. Однако есть еще пара нюансов, на которые я хотел бы обратить внимание.

Типы указания метода

В философии выше указано, что одним из обязательных параметров запроса является его тип. Так мы однозначно говорим серверу, что нужно сделать с данными, которые он получил (например, если этот параметр — registration, сервер попробует на основании остальных параметров зарегистрировать пользователя, отправить ему СМС и так далее).

Однако описанное — лишь один из способов указать серверу на конкретный набор действий. В современной разработке чаще всего используется два варианта:

  1. Указание типа запроса в теле самого запроса (параметром, наш вариант).
  2. Указание типа запроса в URL, по которому идёт обращение.

Если с первым всё более или менее понятно (в нашем случае параметр dt_request может равен либо registration, либо confirm, либо getPoints), то во втором варианте мы в запросах не указываем dt_request вообще, передавая только параметры, нужные серверу для обработки информации. Здесь клиент (мобильное приложение или сайт) будут добавлять к основному URL конкретный адрес, указывающий на тип запроса. Например:

  • запросы с регистрацией будут прилетать на sitename.ru/api/v1/registration/;
  • подтверждение номера — на sitename.ru/api/v1/confirm/;
  • а для получения точек нужно будет постучаться на sitename.ru/api/v1/get-points/.

Вне зависимости от того, какой вы выбрали вариант, я крайне рекомендую именно такой порядок формирования URL вашего API — с указанием версии. Проще будет поддерживать.

Тут, в общем-то, всё. Каких-то особенных преимуществ нет ни у одного из вариантов (хотя кто-то говорит, что второй более наглядный; фиг знает, я люблю первый).

Форматы передачи

Еще одним аспектом является формат передаваемых сообщений (я вкратце об этом писал в третьей части цикла про дизайн данных). Форматов лютая тьма, но самыми популярными на сегодняшний день являются JSON и XML (хотя последний — это целый язык разметки, а не просто формат).

Если упростить, то в данном контексте формат — это то, как именно структурируются данные: как указываются параметры, а как — значения этих параметров.

Вот пример простейшего запроса в XML:

<request>
    <dt_request>registration</dt_request>
    <phone>79558887722</phone>
</request>

А вот — он же, но в JSON:

{
    "dt_request":"registration",
    "phone":79558887722
}

Я стараюсь использовать JSON везде, где это не противоречит здравому смыслу — он проще и нагляднее (когда твои запросы не становятся совсем уж монструозными). Однако у XML есть некоторые преимущества: например, его проще валидировать в автоматическом режиме.

HTTP-методы

Ну и напоследок я расскажу о различиях между POST и GET-запросами. Вообще, с терминологией в IT так себе. Есть методы у HTTP-запросов (как раз эти вот POST и GET), а есть методы у конкретного API (в нашем случае, например, registration, confirm и getPoints). Тут главное не перепутать.

Если совсем упростить, то POST содержит параметры в теле запроса (вон примеры чуть выше), тогда как GET — в URL (с помощью знака вопроса и амперсандов).

Вот пример GET-запроса:

sitename.ru/api/v1/?dt_request=registration&phone=79558887722

Параметры в GET начинаются после знака вопроса (?) и разделяются амперсандами (&): dt_request и phone. Хороший и наглядный пример реализации API через GET можно глянуть на моём сайте рыбатекста.

Просто, да? Нет. Потому что помимо GET и POST есть еще PUT, DELETE, CONNECT и ещё некоторое количество методов. Но истинное их предназначение ныне используется довольно редко. Указанных POST и GET чаще всего хватает за глаза.

Итого

Если кто-то ожидал, что в этой статье я представлю полное описание API нашего мегакрутого приложения про тюленей, то — сорри — нет. Это мы с вами сделаем в следующей, последней статье цикла. Не переключайтесь:

Павел Шерер, продюсер IT-решений

Канал в Telegram

Раньше тут были комментарии, но я решил не плодить сущности. Есть что сказать или спросить — велкам в телеграм-канал:

Обсудить в Telegram