Привет! Соблюдая традиции Хабра, представлюсь. Меня зовут Антон Митрохин. В Тензоре я middle+ разработчик, но недавно мне доверили задачу уровня senior — обратились с просьбой добавить реакции в мобильные приложения. «Хорошая точка роста», — подумал я и согласился. В статье расскажу, как мне вместе с командой удалось реализовать новый функционал.

В чём сложность?

Эмоции (или реакции) — это интерактивные элементы, которые есть почти во всех мессенджерах, но отсутствовали в нашем продукте.

Так теперь выглядят эмодзи в мобильных приложениях продуктов Saby
Так теперь выглядят эмодзи в мобильных приложениях продуктов Saby

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

  1. как данные передавать;

  2. как данные хранить;

  3. как данные обрабатывать.

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

Как передавать данные

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

# Типы сущности, с которыми работает UI и прикладные сервисы

 entity_type = enum {
    news;
    news_comment;
    message;
    forum;
    forum_comment;
 }

Вариантов для передачи таких сущностей существует всего два.

Вариант 1. Передавать эмоции по отдельному запросу

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

Схема передачи данных через два запроса
Схема передачи данных через два запроса

На схеме выше мобильный сервис сообщений пытается сначала получить список сообщений, которые приходят как JSON-модели условно в таком виде:

"Message": {
  "Author": "Пупкин В.В.",
  "Text": "Текст сообщения",
  …
}

А затем, отдельным запросом, пытается получить список эмоций. Снова покажу модель условно:

"EmotionPanel": {
	"MyEmotion": "?",
	"Counters": [
  	{ "Emotion": "?", "Count": 1},
  	{ "Emotion": "?", "Count": 42}
	]
  }

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

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

Схема передачи данных через два запроса без ожидания второго
Схема передачи данных через два запроса без ожидания второго

Думаю, вы уже понимаете проблему. Пользователь зачастую будет видеть отрисовку диалога дважды. Сначала сообщения, потом — счетчики с реакциями. У клиентов явно возникнут вопросы — придется долго и мучительно объяснять, что так задумано. Такой вариант нас тоже не устраивает.

Вариант 2. Передавать эмоции сразу внутри сущности

Схема передачи данных с объединением на стороне облака
Схема передачи данных с объединением на стороне облака

Если облако сообщений будет сразу отдавать модель сообщения вместе с эмоциями, то количество обращений от мобилки к облаку сокращается с двух до одного.

Сама JSON-модель тогда будет выглядеть так:

"Message": {
  "Author": "Пупкин В.В.",
  "Text": "Текст сообщения",
  "EmotionPanel": {
	"MyEmotion": "?",
	"Counters": [
  	{ "Emotion": "?", "Count": 1},
  	{ "Emotion": "?", "Count": 42}
	]
  }
}

Это позволяет быстрее получить данные и отрисовать диалог. Поэтому остановились именно на такой схеме получения реакций к сущности.

Как данные хранить

На предыдущих диаграммах я специально оставил небольшую наживку, а именно сохранение данных из MessageMobileService в локальную таблицу (Save). При любом из двух решений передачи данных из прошлого параграфа возникает проблема: к примеру, когда в микросервис сообщений приходит сообщение, то оно сохраняется в отдельную таблицу. Она никак не связана с микросервисом новостей и его таблицами. У кажого из сервисов есть свои методы для обработки данных: можно, например, отредактировать сообщение, также можно отредактировать и новость — нет смысла писать какой‑то универсальный редактор, потому что ничего общего у них не будет. Теперь вспомним, что мы выбрали вариант передачи данных «вместе с сущностью». Это означает, что теперь каждый прикладной сервис должен уметь работать с эмоциями: менять их, обновлять чужие по STOMP-событию, хранить эти эмоции в локальной таблице сервиса на мобильном устройстве и предоставлять какой-то интерфейс для UI.

Плохой вариант организации
Плохой вариант организации

Красным выделил все те вещи, которые дублируются. Они же относятся к реакциям.

Очевидно, что никому не хочется писать пять реализаций (сущностей, как я уже говорил, именно столько).

Тогда как хранить данные? Централизованно. Это короткий ответ, потому что, если хранить данные одного типа в разных сервисах, то количество потенциальных ошибок и проблем увеличивается пропорционально росту количества таких сервисов.

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

Плохой вариант организации
Плохой вариант организации

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

Для удобства работы прикладных сервисов выделили специальный интерфейс.

EmotionInternalAPI
{
   // Получить панели эмоций по идентификаторам объектов (id сообщения, новости, комментария и т.д.)
   list< EmotionPanel > GetPanels( list< Uuid > object_ids, EntityType entity_type );

   // Сохранение панелей, которые прикладной микросервис мог получить вместе с основными данными (например с сообщениями
   // или с новостями).
   list< EmotionPanel > SavePanels( list< Record > panels, EntityType entity_type );

   // Запустить синхронизацию эмоций
   void SyncAvailableEmotions();

   // Событие для подписки на обновление данных по типу сущности.
   EmotionUpdateEvent OnUpdate( EntityType entity_type );
};

Через этот интерфейс при получении нового сообщения от облака мобильный сервис сообщений просто вырезает из JSON-модели сообщения модель панели реакций и целиком, без какой-либо обработки, отправляет его в мобильный сервис реакций.

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

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

Для удобства работы UI реализовали еще один интерфейс.

class EmotionAPI
{
   // Получить маленький пикер
   EmotionSmallPicker GetSmallPicker( Uuid object_id, EntityType entity_type );

   // Получить "большой пикер"
   EmotionBigPicker GetBigPicker( Uuid object_id, EntityType entity_type );

   // Поиск эмодзи по полному списку
   list< EmojiDescriptorGroup > Search( Uuid object_id, String search_query, EntityType entity_type );

   // Изменить мою эмоцию
   void ChangeMyEmotion( Uuid object_id, String my_emotion, EntityType entity_type );

   // Подписаться на обновление
   void EnableSubscribeOnUpdate( EntityType entity_type, Uuid group_id );

   // Отписаться от обновления
   void DisableSubscribeOnUpdate( EntityType entity_type, Uuid group_id );
};

Через этот интерфейс UI может изменять свою эмоцию, выполнять поиск или подписываться на обновления реакций.

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

Основные модели выглядят так:

emotion_panel, smal_picker и big_picker слева направо
emotion_panel, smal_picker и big_picker слева направо

Панель эмоций - модель, которая отображается статично (т.е. ее видно без клика):

emotion_panel = record {
   # ID объекта, к которому привязана панель (id сообщения, новости, комментария и т.д.)
   [req] object_id: uuid;

   # Выбранная эмоция
   selected: string;

   # Список эмоций, которые были выбраны + популярные, которые еще не выбраны
   emotions: list< emotion_counter >;
}

 Маленький пикер - это view-модель, которая открывается по клику на панель эмоций

emotion_small_picker = record {
   # ID объекта, к которому привязана панель (id сообщения, новости, комментария и т.д.)
   [req] object_id: uuid;

   # Выбранная эмоция
   selected: string;

   # Список эмоций, которые были выбраны + общепопулярные(статичный список для всех), которые еще не выбраны.
   emotions: list< emotion_counter >;

   # Есть еще эмоции, которые не попали в список, нужно отобразить '+' для открытия BigPicker'а
   has_more_emotions: bool;
}

Большой пикер – это view-модель, которая открывается по клику на маленький пикер

emotion_big_picker = record {
   # ID объекта, к которому привязана панель (id сообщения, новости, комментария и т.д.)
   [req] object_id: uuid;

   # Выбранная эмоция
   selected: string;

   # Список эмоций, которые были выбраны + популярные, которые еще не выбраны
   emotions: list< emotion_counter >;

   # Список последних эмоций
   last_emotions: list< emoji_descriptor >;

   # Список доступных эмоций
   available_emotions: list< emoji_descriptor_group >;
}

Как данные обрабатывать

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

Для обновления реакций на лету реализовали систему подписки на STOMP-события, которая отслеживает видимые элементы на экране телефона и следит за их обновлением.

Что в итоге?

  • Быстро и качественно вместе с коллегами организовал работу между командами облака android, iOS и контроллера (наше ядро для мобильного UI).

  • Вместе с командами продумал API для каждой из них.

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

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

  • Полностью реализовал новый сервис и поддержал изменения в нескольких прикладных сервисах.

Что осталось

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

P.S. Если считаете, что моя работа тянет на работу выше, чем middle, можете написать об этом в комментариях, может кто-то из руководителей увидит ?

Комментарии (0)