Из вагона направо: как работают подсказки 2ГИС

Весной мы добавили новую подсказку о том, в какую сторону выходить из вагона метро. Я Влад, программист С++, и на примере этой подсказки хочу рассказать, из чего состоит жизненный цикл релиза новых фич в 2ГИС. И сколько всего происходит, когда добавляешь — казалось бы — небольшую новую строку с текстом.

c25959e4dc37079ed32fd87d052d0397.jpg

Выбираем фиче-крайнего

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

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

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

Собираем продуктовые требования

df2e816278a1c2f41ece63a48f8a32fc.jpg

Мобильная версия 2ГИС уже подсказывает пользователю, в какой вагон метро ему стоит сесть: в первый, ближе к началу/концу, в середину, в последний.

Наш продакт предложил добавить к этой подсказке ещё одну: о том, в какую сторону удобнее выйти из вагона. К счастью, он также написал ТЗ, которое выглядело так:  

  1. Сопровождать выход пользователя из первого вагона подсказкой «Выходите из вагона направо», а выход из последнего — фразой «Выходите из вагона налево».

  2. Уметь обрабатывать «перевёрнутые» платформы.

  3. Подсказка должна быть отключаемой для любой платформы.

Почему справедлив первый пункт, можно проиллюстрировать следующим образом:

Сторона выхода зависит от вагона посадки. Сел в первый — вышел направо, сел в последний — вышел налево.Сторона выхода зависит от вагона посадки. Сел в первый — вышел направо, сел в последний — вышел налево.

Что означает второй пункт ТЗ, что за «перевёрнутые» платформы?

На «перевёрнутых» платформах сторона выхода из вагона не совпадает с той, с которой человек заходил в вагон. Таких платформ не очень много, но их все равно нужно учитывать. Поясним на примере:

Сел в первый — вышел налево, сел в последний — вышел направо, ситуация противоположная той, что на предыдущей картинкеСел в первый — вышел налево, сел в последний — вышел направо, ситуация противоположная той, что на предыдущей картинке

В принципе всё понятно, и хотелось бы написать: «Мы сделали всё по ТЗ», закончив на этом статью. Однако есть нюансы :)

Декомпозируем задачу

Мобильное приложение 2ГИС умеет работать онлайн и офлайн. 

Если у пользователя есть подключение к сети

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

Если доступа к сети у пользователя нет

В этом случае поиск маршрута происходит внутри самого приложения. Это фолбэк-стратегия поиска (от англ. fallback — резервный вариант). 

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

Естественно, наша новая подсказка должна работать в обоих случаях.

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

  1. Поисковой сервер 2ГИС должен уметь генерировать новую подсказку: это задача для команды алгоритмов/бэкенда.

  2. В мобильном приложении новая подсказка должна отображаться на экране пользователя путём парсинга json-ответа от сервера либо генерироваться в приложении: это задача для команды мобильной разработки.

На самом деле, нет необходимости раздельно генерить подсказки на сервере и в мобильном приложении (клиенте), поскольку за поиск маршрутов на общественном транспорте отвечает одна и та же поисковая библиотека, написанная на С++. Когда сервер 2ГИС получает запрос от клиента о построении маршрута на общественном транспорте, он использует эту библиотеку для поиска маршрутов, а затем сериализует результат в json-ответ, который отправляет клиенту. Если работа с сервером по каким-то причинам недоступна, то клиент обращается к той же библиотеке, которая слинкована с мобильным приложением, и получает результат. Конечно, сериализовывать этот результат в json в этом случае не имеет смысла. А значит, требования к реализации выглядят следующим образом:  

  1. Новая подсказка должна генерироваться в библиотеке поиска маршрутов на общественном транспорте (задачка для бэкенда).

  2. Поисковый сервер должен уметь добавлять подсказку в свой json-ответ (задачка для бэкенда).

  3. Подсказка должна визуализироваться в мобильном приложении по json-ответу и по ответу от поисковой библиотеки (задачка для мобильных разработчиков).

Фиксация API

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

Что делать с иконкой? Парсить текст подсказки, чтобы понять направление и визуализировать стрелочку, — не самое изящное решение. Решили просто добавить в ответ сервера ещё одно поле — сериализация Direction в одно из двух слов «right»/«left».

eb94d2b21a97adcc2fb89051128a24ed.png

Новые поля в ответе сервера

Когда сервер генерирует «landing_suggest», поле будет формироваться сразу в локали пользователя, которую сервер получает в клиентском запросе. Для этого сервер обратится к словарю фраз требуемой локали, чтобы преобразовать Direction во фразу на русском «выходите из вагона налево/направо» (либо на другом из десятка поддерживаемых языков). 

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

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

Готовим и упаковываем данные

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

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

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

Собираем и отдаём данныеСобираем и отдаём данные

Раз речь зашла о данных, значит, мы должны что-то в них поменять. 

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

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

  1. Нужно доработать сервис упаковки данных

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

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

Налево или направо?

Сама по себе генерация новой подсказки в библиотеке поиска маршрутов — не rocket science. В библиотеке за генерацию всевозможных подсказок по найденным маршрутам отвечает отдельный класс SuggestsGenerator. Интерфейс этого класса можно упрощённо выглядит так.

class SuggestsGenerator
{
public:
	// Генерирует подсказки для всех маршрутов из списка
	Routes Generate(const Routes& routes) const;
}

У этого класса также есть доступ к интерфейсу файла роутинга — нашему бинарному пакету, где лежат атрибуты каждой платформы метро города, в котором строится маршрут в данный момент.

Найденные маршруты передаются в функцию Generate (), которая возвращает модифицированную версию маршрутов с заполненными полями подсказок. Каждый найденный маршрут состоит из последовательности перемещений, где перемещение — некоторый участок маршрута на одном виде транспорта. Это может быть проезд на автобусе, или, например, проезд на метро от начальной платформы до конечной. 

Маршруты ABCD, AB1C1D между А и D. Перемещения — звенья ломаныхМаршруты ABCD, AB1C1D между А и D. Перемещения — звенья ломаных

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

Чтобы сгенерировать подсказку о стороне выхода из вагона, нужно учесть несколько условий:  

  1. есть ли подсказка о вагоне, в который пользователю предложили сесть;

  2. есть ли у платформы требуемые атрибуты в данных;

  3. показывать ли подсказку для платформы (проверяем значения атрибута do_not_show);

  4. является ли платформа «перевернутой» (используем атрибут is_reversed).

Весь код бизнес логики уместился в одну функцию:

std::optional MakeSuggest(const Movement& m) const
{
	// Проверяем, что m - перемещение на метро
	// Проверяем пункты 1-4
	// Возвращаем std::nullopt, если не удалось создать подсказку 
}

где Direction — это простой enum class:

// Описывает направление высадки из вагона метро
enum class Direction
{
	// Выход из вагона направо
	Right,
	// Выход из вагона налево
	Left
};

А теперь применим новую функцию внутри SuggestGenerator: Generate (псевдокод):

Routes SuggestsGenerator::Generate(const Routes& routes) const
{
	Routes new_routes(routes);
	
	for(auto& r: new_routes)
	{
		// Пробежимся по перемещениям маршрута
		for(auto& m: r.movements)
		{
			m.suggest = MakeSuggest(m);
		}
	}
	return new_routes;
}

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

На стороне сервера были добавлены те два поля нового API, о которых было сказано выше, путем простой сериализации сгенерированных Direction«ов для каждого перемещения.

Как это тестировать?

Чтобы протестировать новую фичу, нужен набор компонентов:  

  • тестовый сервер;

  • тестовый сервис упаковки данных в файл роутинга;

  • сам файл роутинга с новыми атрибутами платформ метро для сервера;  

  • тестовое мобильное приложение.

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

Сначала протестировали мобильное приложение без доступа к сети, предоставив ему пакет с новыми данными. Затем подвергли тщательной проверке сервис упаковки данных, предоставляя ему как данные с атрибутами платформ, так и без них. Таким образом собрали новый пакет по одному городу, где много разных платформ метро. Угадайте, какой это город?:) 

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

В целом, при тестировании клиента и сервера не было проблем, кроме одной: первая буква фразы подсказки в приложении была строчной вместо прописной. Что ж, мы обратились к уже существующим подсказкам в 2ГИС и сделали также: через капитализацию на стороне приложения.

Зарелизили фичу, которой нет

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

Когда релизы случились, я обновил приложение на телефоне и начал искать заветную подсказку, но не находил. Я помнил некоторые тестовые сценарии и точно знал, где должна быть подсказка! Это было печально, и я отправился на поиски причины недоразумения. «Хорошо, что хоть сервер работает», − успокоил себя этим. 

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

По ту сторону релиза

Это фича — наглядный пример того, что порой интегрировать все компоненты фичи воедино сложнее, чем написать отдельную функциональность каждого компонента. Реализация подсказки о выходе из вагона метро потребовала согласованной работы трёх команд и выпуска трёх релизов. Это был интересный опыт, заставивший меня погрузиться в те области работы в 2ГИС, с которыми я ещё не сталкивался. После того, как был выпущен релиз сервиса подготовки данных и были подготовлены файлы роутинга, подсказки о выходе из вагона метро появились у пользователей, в том числе и у меня. А это значит, что теперь мы знаем, в какую сторону из вагона метро нам всем идти :)

© Habrahabr.ru