Дизайн данных (часть 3). Меняемся?

Третий пост, посвященный дизайну данных. Что такое API, как и зачем проектировать обмен данными между компонентами системы.

10-13 минут на прочтение
Дизайн данных (часть 3). Меняемся?

Вы читаете третью статью из цикла «дизайн данных», она посвящена обмену данными между компонентами системы. Чтобы вообще понять, что здесь происходит, я рекомендую ознакомиться с первыми двумя.

Освежаем память

Итак, в прошлом посте у нас появилось великолепное приложение, которое позволяет отслеживать миграцию полосатых тюленей на берегах Охотского моря. Внутри у нас есть регистрация/аутентификация пользователей, список точек, кэширование изображений.

Среди архитектурных компонентов мы рассматриваем только мобильное приложение и сервер (middleware). Нам не важно, откуда берутся данные о миграции (админка ли это, внешняя интеграция — плевать). Также мы не станем описывать запросы к сервису СМС-рассылок, которые будут генерить необходимый для регистрации код. Наша задача — научиться в data design, а не спроектировать полноценный сервис.

Базовая структура данных приложения выглядит вот так:

Красной и желтой областями указано, где именно хранятся данные.

Теперь нам нужно придумать, каким образом клиент (мобильное приложение) будет перекидываться запросами с сервером. Для этого сперва чуток окунемся в матчасть.

Страшное слово API

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

На самом деле, в большинстве «апишек» нет ничего сложного — при условии, что они хорошо документированы; а дизайнер, изучающий их, малость прошарен в технической стороне вопроса.

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

Разумеется, я не стану погружать вас в пучину кодинга и рассказывать о сигнатурах, функциях, классах. Мы обойдем даже такие популярные темы, как REST и «физические» случаи, вроде Keyboard API. Особо упорные могут копнуть эти темы самостоятельно — или же дождаться следующих постов цикла про дизайн данных.

Методы

Начнем, собственно, с уже упомянутых методов. По сути, это программно описанные способы взаимодействия компонентов (например, сервера и сайта).

Даже вот так проще: каждый запрос на сервер содержит в себе метод, в котором описывается, какие именно данные нужно получить. Если у метода есть параметры — они тоже указываются в запросе. И ещё кое-что, но об этом ниже.

Важный момент: не путайте методы API и HTTP-методы.

Как правило, каждая отдельная операция имеет свой собственный метод. В нашем приложении методами API будут:

  • регистрация/аутентификация;
  • подтверждение номера телефона (код из СМС);
  • получение списка точек;

Заметьте, мы не сделали два отдельных метода: получение и обновление списка точек. Первичное получение списка точек и подгрузка точек при скролле получаются с помощью одного и того же метода — просто мы указываем разные параметры в этих двух запросах. Оба раза нам возвращается одинаковый тип данных: список точек.

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

Но тут есть один нюанс. Если потом по какой-то причине «вход» станет отличаться от регистрации сценарно, мы с высокой вероятностью воткнемся в необходимость куда больших доработок. А значит, повысится стоимость реализации.

Иногда название метода API зашивается в адрес (например, https://<...>/points/get/), иногда — указывается прямо рядом с параметрами запроса, а порой даже эти два способа комбинируются. Мы, чтобы не усложнять, будет рассматривать только второй вариант.

Параметры

Здесь все просто. Именно в параметрах мы указываем дополнительную информацию для сервера. Как правило, параметр имеет пару «ключ: значение». Например, «name: Вася», где «name» — это ключ, а «Вася» — значение.

Иногда в запросе указывается только метод, без параметров. Помните, в прошлой статье мы говорили о свойствах списка точек? Если мы договоримся, что по умолчанию отступ всегда равен нулю, а область видимости — двадцати, то при запросе этого списка с сервера мы можем указать только название метода, без параметров. Тогда сервер вернет нам последние двадцать точек.

Если вы внимательно читали предыдущую статью, то должны помнить, что свойства сущностей могут быть сложными, включающими в себя несколько значение. С параметрами этот фокус тоже работает — вы можете указать внутри одного ключа ещё несколько пар «ключ: значение».

Заголовки

Это такая магическая штука, которая позволяет нам не дублировать одну и ту же информацию в каждом методе.

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

Мы можем, конечно, каждому методу добавить дополнительные параметры, за это отвечающие. А также версию приложения, платформу и тьму прочих данных. Представляете, как распухает каждый метод? У нас пойдет дублирование — и если потом мы решим, что для аналитики нам нужно с каждым запросом передавать ещё что-то… Да, придется переписывать все методы как на сервере, так и на клиенте. Не очень удобно, дорого и ненадежно.

Кроме того, заголовки почти всегда несут в себе тьму служебной информации, которая необходима принимающей стороне для классификации и правильной обработки запросов/ответов — но сейчас, разумеется, нас это не будет тревожить.

Ошибки

Мы не станем подробно останавливаться на этом пункте, хотя он заслуживает отдельного раздела, книги, оды и дифирамбов. Если бы вы знали, сколько денег, сил и времени человечество потратило из-за плохой унификации и неверной обработки ошибок API — вы бы не удержались от слёз.

Ошибки, их типы, коды и способы обработки должны быть продуманы заранее. Вы банально не сможете унифицировать UI, если к каждой новой ошибке вам придется рисовать новое состояние формы, сообщение-плашку или даже целый экран.

В двух словах об этом не рассказать. На текущем этапе будет достаточно, если вы просто будете знать, что ошибки нужно делить на типы (например, «ошибки получения данных», «ошибки авторизации» и тп), и внутри каждого типа создавать список кодов каждой ошибки (например, «неверный код из СМС», «пользователь не найден», «секретный ключ устарел» и так далее). Разумеется, приложение должно уметь работать с каждым типом и кодом. И чем более универсально — тем лучше.

Схема

Итого, очень упрощенная схема запросов (requests) и ответов (responses) выглядит так:

Безусловно, это не отражает всех возможных вариантов. Параметры могут вообще указываться в URL, и тогда тело запроса окажется пустым. Просто знайте это.

Запросы к тюленям

Итак, вводная часть закончена, подбираемся к самому вкусному — примерам. Разумеется, в качестве основы возьмем наше выдуманное приложение для отслеживания миграции самых милых в мире млекопитающих.

Структура запросов

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

В нашем случае каждый раз мы будем передавать:

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

Поэтому эти данные мы вынесем в заголовки. Всё остальное пускай отправляется в теле запроса.

Вот так у нас будет выглядеть первый набросок схемы запросов к API:

Я не стану дополнять эту схемку ответами (responses), так как это довольно простая часть. Запросы же (requests) буду вносить постепенно, чтобы сформировалась общая картина. Если кому-то вдруг понадобится визуальное представление ответов сервера, содержащих свойства сущностей (например, точек миграции), у него всегда есть возможность вернуться в начало статьи и еще раз рассмотреть изображение со структурой данных.

Регистрация и аутентификация

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

  1. Со стороны нашего сервера к внешнему сервису СМС-рассылок отправляется запрос на формирование кода, сервис возвращает этот код. Эту часть детально описывать не будем.
  2. Сервер ищет в БД пользователя с указанным номером телефона.
  3. Если пользователь не найден:
    1. Формируются два идентификатора пользователя:
      • внутренний (для собственных нужд);
      • глобальный (для общения с миром).
    2. В базу данных на сервере записывается:
      • полученный номер телефона;
      • код, который вернул сервис;
      • дата и время регистрации;
      • внутренний идентификатор;
      • глобальный идентификатор.
  4. Если пользователь найден в БД по номеру телефона, то на этом шаге мы просто обновляем ему код, который нам вернул СМС-сервис.
  5. В мобильное приложение возвращается информация о том, что «всё хорошо, жди кода в СМС» (статус) и глобальный идентификатор пользователя. В случае ошибок (например, СМС-сервис вдруг сдох), мы возвращаем в приложение соответствующее сообщение.

Таким образом, на сервер мы отправляем номер телефона, а оттуда ждем статус и глобальный идентификатор (который записывается локально на устройстве пользователя).

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

Добавляем первый метод в схемку, указывая единственный параметр, который в него передаем:

Подтверждение номера телефона

Пользователь вводит код из СМС и отправляет его на сервер. Чтобы нам понять, кто вообще прислал нам этот код, мы вместе с ним отправляем глобальный идентификатор пользователя, полученный на предыдущем шаге, а также — идентификатор устройства.

Далее, на сервере:

  1. Осуществляется поиск в БД по полученному идентификатору пользователя.
  2. Если пользователь не найден, на клиент возвращается сообщение об ошибке.
  3. Если пользователь найден, полученный код сверяется с записанным в БД.
    1. Если коды не совпадают, в приложение снова улетает ошибка.
    2. Если коды совпадают:
      1. Формируется секретный ключ.
      2. В базу данных записывается сформированный только что ключ и полученный идентификатор устройства. Причем записываются они в связке, взаимозависимыми.
  4. В мобильное приложение возвращается сообщение о том, что «всё ok» и секретный ключ.

Итого, клиент отправляет код из СМС, глобальный идентификатор пользователя и идентификатор устройства, а получает статус и секретный ключ (его, как и глобальный идентификатор, мы сохраняем локально).

На этапе формирования запроса у нас еще нет секретного ключа, поэтому в запросе соответствующий заголовок остается пустым.

Всё, вроде, просто — но только если вы понимаете общую структуру данных. Будь наш пример чуть сложнее, то на уровне чистого (незамутненного техническими заморочками) юикса можно было бы нагородить такие костыли, что бедные разработчики потом умывались бы кровавым потом. Или напротив, сделали бы «по-своему», со всеми вытекающими.

В любом случае, метод и параметр улетели в общую схемку:

Получение списка точек

Самый объемный метод, включающий в себя не только формирование и получение списка точек, но и пару нюансов, однозначно заслуживающих внимания. Начнем с них.

Авторизация

После того, как пользователь зарегистрировался или вошел в аккаунт, каждый запрос на сервер должен быть подтвержден секретным ключом. Ибо чужим не место в нашем уютном мирке наблюдателей за тюленями.

Итак, для авторизации мы в заголовках запроса передаем секретный ключ. Включаем paranoiac mode и (как уже решили выше) добавляем к запросам уникальный идентификатор устройства. Как мы помним, они у нас связаны на сервере — как раз для того, чтобы секретным ключом не могли воспользоваться с другого девайса плохие дяди.

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

Инициализация

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

Также при инициализации может происходить проверка на устаревшие данные (например, профиля) для обновления кэша. Или проверять статус синхронизации состояний, если ваш продукт поддерживает бесшовное взаимодействие.

Инициализация рекомендуется везде и всегда, однако в нашем случае его функцию будет выполнять запрос на получение точек — просто потому что других запросов от авторизованных пользователей у нас нет. А пилить инициализацию для неавторизованных в нашем простейшем случае явный перебор.

Собственно, получение точек

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

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

Получив запрос, сервер первым делом проверяет версию приложения и, если оно устарело, сообщает об этом. Нам важно, чтобы наблюдатели за тюленями пользовались свежим софтом. Если все нормально, сервер ищет пользователя в БД по глобальному идентификатору:

  1. Если пользователь найден, секретный ключ, полученный в запросе, сравнивается с аналогом в БД у пользователя:
    1. Если ключи совпадают, проверяется соответствие идентификатора устройства из запроса с сохраненным в БД (и связанным с секретным ключом):
      1. Если идентификаторы не совпадают, сервер формирует ответ с ошибкой и отправляет его в мобильное приложение.
      2. И только если идентификаторы устройств совпадают, происходит формирование списка точек: осуществляется запрос к БД, в котором в качестве параметров указываются отступ (по факту, количество дней от начала, которые надо пропустить) и область видимости (просто количество дней, за которые нужно получить данные). Полученный массив, вместе со статусом «всё ok», отправляется на клиент.
    2. Если ключи не совпадают, сервер прерывает операцию и сообщает об этом мобильному приложению.
  2. Если пользователь не найден, сервер возвращает сообщение об ошибке.

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

Точки приходят нам простым списком, отсортированные по времени. Мы обрабатываем их и «пакуем» по дням, чтобы нормально отобразить в интерфейсе. Всё.

Вот наша финальная схемка запросов:

К слову об ошибках. Посмотрите, сколько в сценарии выше их может возникнуть. Целых 4 штуки, не считая технических (типа «сервер упал», «нет интернета» и тп). А в мобильном приложении каждую из них нужно грамотно обработать и показать пользователю. И это суперпростой пример.

Оптимизация — сестра UX

Готово, обмен совершен. Всё работает, пользователь доволен. Но давайте малость отмотаем назад, до регистрации/аутентификации. Не кажется, что можно что-то оптимизнуть? Вот у нас прошел запрос на подтверждение номера телефона, мы получили секретный ключ — и выполняем новый запрос, на получение точек. Зачем? Что будет видеть пользователь всё то время, пока выполняются два запроса подряд? Лоадер, заглушку, плейсхолдеры?

Ведь мы знаем, что после успешных регистрации или аутентификации всегда показывается экран со списком точек. Что будет, если в параметры запроса с кодом из СМС мы сразу включим отступ и область видимости? О да, мы сделаем на один запрос меньше, а наш горячо любимый юзер получит результат на секунду-две раньше. Если хотите, попробуйте расписать сценарий обмена данными с учетом этого — простенькое упражнение, но оно гарантирует, что посыл этой статьи выветрится у вас из памяти несколько позже.

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

Колдунство

Прочитав этот пост и вдоволь погуглив, можете копнуть остальные составляющие запросов и другие технические детали обмена данными. Тут уже вас ожидает полное мерлинство: это и уже упомянутые HTTP-методы, и коды статусов, и сжатие ответов, и схемы/корсы/юзер-агенты, и прочие адовы полчища терминов, технологий и возможностей. Если же у вас остались вопросы — смело задавайте их в комментариях. Настоящий дизайнер не должен быть стеснительным.

Что дальше?

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

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

Канал в Telegram

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

Обсудить в Telegram