Предыстория
Будучи студентом я периодически ездил домой на поезде. Так как в мои края ездило всего лишь два прицепных вагона, о билетах приходилось заботиться заранее. Естественно, это было крайне неудобно и я, молодой и зеленый, быстро написал небольшое приложение на Qt Framework, которое ходило не сервер УЗ и проверяло наличие билетов. Как только появлялся заветный билет, приложение отправляло смс.
Шли годы, появился Android, а затем и Necessitas – порт Qt для Android. Недолго думая, я взялся за портирование приложение на платформу Android. Работа была быстро сделана и результат был опубликован в Google Play. И тут меня ожидало разочарование… Огромное количество ошибок в Necessitas, не родной интерфейс Android, сложность установки и еще куча других причин сделали свое дело. Оценки получались в основном негативными и руки у меня быстр опустились. Обиженный на весь мир я удалил приложение с Play и на какое-то время забыл…
История
В августе 2012 я решил изучить Android SDK. Я вооружился парой паршивых книг, прочитал их, будто они художественные, и сел за компьютер. Что бы научиться писать, нужно писать. Но что бы такого написать? Ага, заветное приложение для проверки наличия билетов.
Я стал перебирать все сайты, которые предлагали приобрести билеты УЗ и РЖД. Их оказалось довольно много. На одном из сайтов я набрел на страницу с предложение о сотрудничестве. Быстро связался с ними и они предложили мне RESTful API к их серверам (в дальнейшем А). Все что требовалось – это подписать пару различных договоров. Я решил, что ничего не теряю, а возможность приобрести билеты через приложение будет неплохим бонусом.
Пока тянулась вся бумажная волокита, неугасаемый пыл не давал сидеть на месте. Было решено написать приложение используя API booking.uz.gov.ua (в дальнейшем В) и pass.rzd.ru (в дальнейшем С). Благо реверсить API – дело не сложное, и в конце октября было опубликовано приложение, позволяющее проверять наличие билетов через сервер В. Затем было обновление, которое добавило поддержку сервера С.
Время тянулось медленно, а с полученным API возникли технические проблемы, решение которых происходило не так быстро, как хотелось. Приблизительно где-то в конце декабря была готова новая версия Android приложения, которая позволяла искать и покупать билеты УЗ через сервер А.
Но запускать обновление я не спешил: что-то во всем этом мне не нравилось. И это что-то называлось «зависимость». Я осознавал, что я буду полностью и всецело зависеть от сервера А. Если вдруг придется разорвать все отношения с партнером, то приложение в один миг превратится в тыкву. И ладно бы отвалилась проверка/покупка билетов – можно же переехать на другой сервер. База данных пользователей и вся их история покупок канет в Лету.
Нужен был совет более опытных людей. Я встретился с другом, у которого есть большой опыт проектирования различный систем, и изложил ему суть проблемы. Он и глазом не моргнул, просто выдал одну фразу «ты еще мал и глуп».
Суть нашего длинного диалога можно уместить в одно предложение: «Ты пытаешься написать приложение, а надо думать более масштабно, нужно думать о предоставлении услуги». Было предложено написать серверное приложение, которое являлось бы посредником между Android приложением и внешними провайдерами. Тогда вся база пользователей и билетов была бы у меня на сервере. Падение или отключение провайдера ЖД не ломало бы приложение. Переезд на другого провайдера или подключение других сервисов, к примеру, авиабилетов, было бы полностью прозрачно для пользователя Android приложения.
Все это звучало конечно же превосходно, но вот писать серверное приложение морально я был не готов. Но и выпускать обновление я уже точно не собирался. Друг предложил свою помощь в написании букинг (прошу простить за англицизм) сервера и мы принялись за дело.
Серверное приложение
Веб сервис написан с использованием Spring MVC. Приблизительная схема компонентов и их взаимодействия:
Бизнес логика
Начнем рассматривать систему с ее ядра, а именно, с бизнес логики. Это одна из самых сложных частей букинг сервера. И дело даже не в реализации. Самые большие проблемы встали в процессе проектирования внешних API. Дело в том, что перед серверным приложением ставилась задача не поискапродажи именно ЖД билетов, а поискпродажа различных билетов, будь то билеты на поезд, автобус, самолет, концерт или кинотеатр. Задача оказалась совсем не тривиальной. Очень много часов пришлось просидеть, чтобы описать все возможные варианты работы с различными билетами, процедить это и получить универсальное, минимальное API.
В общем случае поисковая модель получилась такая:
Результатом любого поиска являются туры. Каждый тур содержит в себе как минимум одно путешествие. Путешествием может быть как поездка на транспорте так и поход в кинотеатр. Пример тура:
- «Путешествие 1» — Поезд из Севастополя в Киев
- «Путешествие 2» — Посещение концерта поп-звезды
- «Путешествие 3» — Самолет из Киева в Симферополь
- «Путешествие 4» — Автобус из Симферополя в Севастополь
Таким образом мы можем составлять любые маршруты передвижения. Будь то рейсы туда-обратно, маршруты с пересадками. Или, например, человек ищет поезд из А в Б, а мы ему можем еще предложить посетить популярное культурное мероприятие в точке Б.
Каждому путешествию принадлежит один «Исполнитель». Это может быть как транспортное средство, так и кинотеатр или концертный зал. У исполнителя есть так называемые «Залы». В поезде – вагоны, в самолете – классы, в кинотеатре – залы (красный, синий и т.д.) на концертной площадке – зоны (фан-зона, зона столиков и т.д.). А вот уже в залах есть места.
Описывать модель бронирований я не стану, но хочу отметить, что там все не совсем просто. Надо уметь выполнять бронирования одновременно на разных исполнителей в разных залах для разных людей. А с учетом того, что провайдеры могут отдавать неделимые коды бронирования (например, один код для группы людей), надо быть готовым к большому количеству различных вариантов храненияобработки данных. Еще же есть электронные регистрации, возврат билетов и т.д. и т.п… Вообщем есть над чем подумать.
К бизнес логике в придачу идет другой компонент, модель данных бизнес логики (BLModel), которая фактически используется во внешнем интерфейсе для реализации отображения (view). В нашем случае был реализован компонент REST, но никто не запрещает завтра добавить другой компонент, например, для работы с сервисом через веб сайт.
RESTModel
Так же в состав серверного приложения входит модель данных для связи между компонентом REST и мобильным приложением – RESTModel. Фактически, приложение на телефоне и серверное приложение обменивается объектами RESTModel. Создаем java объект, сериализируем его в JSON, отправляем на сервер, в ответ получаем JSON-объект, десериализируем его в java объект. Для сериализации/десериализации на Android мы использовали библиотеку Gson. После того, как такая схема была реализована, мы столкнулись с парой проблем:
- аннотации в классах модели;
- модель на языке java.
Как вы понимаете, модель данных для сервера и телефона одна и та же. Мы просто экспортируем jar файл, как библиотеку, в Android приложение и пользуемся ей. Но когда мы стали добавлять в классы модели аннотации, наш проект для android, потребовал в зависимости библиотеки, с самими аннотациями. И кажется, решение довольно простое, подложить недостающие библиотеки в android проект. Чем это может обернуться? Увеличившимся размером приложения. Мы тащим в приложение совсем ненужные ему библиотеки.
Мы погоревали, погоревали и закрыли на это глаза. А зря. Уже не помню точно виновника, но нам понадобились пару аннотаций из пакета javax.*. И вот тут, android проект радостно нам сообщил о том, что добавлять в приложение классы из пакета javax.* нельзя. Эдакое архитектурное ограничение для виртуальной машины dalvik. Выхода из этой ситуации мы видели три:
- Обойтись без аннотаций из пакета javax.*. В конечном счете, как временное решение мы пошли этим путем. Но, как говорится, нет ничего постояннее чем временное.
- Стрипать! Т.е. генерировать не одну библиотеку rest_model.jar, а еще одну rest_model_striped.jar. Первая бы использовалась сервером, а вторая, без аннотаций android приложением. Решение конечно интересное, не нам крайне не хотелось изобретать велосипед, рыться в гугле и модифицировать процесс сборки. Мы отказались и от этого варианта.
- Вынести аннотации в xml конфиг. Т.к. мы использовали Spring Framework, где уже существует подобный механизм, мы вполне могли остановиться на этом варианте.
В чем же проблема модели на языке java? А в том, что когда мы стали размышлять о портировании приложения на другую платформу, java нас не устраивала. Нам нужна была модель на другом языке. Да, тот путь, которым мы пошли – небольшая архитектурная ошибка. Но что сделано, то сделано. Т.к. модели – очень простая штука (поля + геттеры/сеттеры), то легко можно скормить их какому-нибудь конвертеру по типу Java -> C# или Java -> C++. Но если у вас есть идеи получше, тогда ждем комментариев к статье.
Источники данных
Теперь давайте бегло пробежимся по источникам данных для нашей бизнес логики, а именно ExternalProviderAPI. Если у нас появляется новый провайдер (скажем, автоперевозчик), все что от нас потребуется, это реализовать интерфейс ExternalProviderAPI. Да, это все. Больше никаких хитрых манипуляций и модификаций в бизнес логике проводить не надо. Только реализовать интерфейс внешнего провайдера. Таким образом, например, совсем недавно был добавлен плагин РЖД. Давать описание универсального интерфейса ExternalProviderAPI здесь я пожалуй не стану.
Мультиязычность
Отдельно хотелось бы коснуться вопроса мультиязычности. Об этой проблеме мы позаботились заранее еще на этапе проектирования. Реализация довольно простая, при запросе данных указывается язык, на котором хотелось бы получить ответ и бизнес логика делает все возможное, чтобы удовлетворить запрос клиента. Тут стоит помнить два момента.
Во-первых, ваш провайдер может не отдавать данные на языке, на котором были они были запрошены. Давайте пофантазируем, допустим Укрзализныця отдает данные на четырех языках: русский, украинский, английский и немецкий. А РЖД только на двух: русский и английский. Тут можно пойти двумя путями:
- Если клиент запросил данные на языке X, а провайдер этот язык не поддерживает, то он вернет данные на языке по умолчанию, скажем на английском.
- Отдавать данные не на языке по умолчанию, а на языке, который с большей вероятностью будет более понятен человеку, который знает язык Х. Вернемся к примеру с ЖД. Допустим клиент запросил данные на украинском языке. В такой ситуации логичнее будет вернуть результаты провайдера РЖД на русском языке. А если будут запрошены данные на немецком языке, то вернуть данные на английском. Мы заложили в серверное приложение этот вариант, но в мобильном приложении пока мультиязычности нет.
Во-вторых, представьте ситуацию: человек установил русский язык по умолчанию и купил билет Москва — Санкт-петербург. Вы успешно все сгрузили в базу данных и готовы отдать информацию по первому требованию. Затем этот человек переключает язык на английский и просит у вас список купленных билетов. На каком языке будут отданы станции «Москва» и «Санкт-петербург»?
- Самый простой вариант, как из базы достали, так клиенту и отдали. Приобрел человек билет на русском языке, в базу сохранились значения на русском языке. Мы решили не сильно ломать голову и воспользовались именно этим подходом.
- Сохранять в базу не фактические названия станций, а ссылки на станции, допустим в другую таблицу, где хранятся названия станций в различной локализации.
Хранение данных
Следующий вопрос, который хотелось бы поднять, это вопрос, касающийся хранения данных. Первое, что хотелось бы отметить, в веб сервис была заложена идея субагентской схемы. Это значит, что мы можем выдать ключ и документацию субагенту, а тот в свою очередь может работать через наш сервер абсолютно прозрачно, как будто он – единственный пользователь системы. На каждого субагента выделяются свои базы данных. Их, к слову, в приложение целых две. Одна для хранения постоянных данных: пользователи системы, купленные билеты и т.д. Вторая, для хранения временных данных – результатов поисковых сессий. В роли первой сейчас работает MySQL, в роли второй — file in memory. Внутри бизнес логики все операции с данными выполняются через интерфейс Spring CrudRepository.
Гостевые пользователи
Для нормальной работы бизнес логики необходимо четко идентифицировать пользователя, который выполняет поисковые сессии. Решается это довольно просто – с помощью регистрации и токенов. Но заставлять пользователя регистрироваться со старта приложения – довольно глупая затея. Тогда мы прибегнули к гостевым пользователям. После первого запуска приложение автоматически выполняет гостевую регистрацию и получает токен, с которым затем ходит на сервер. Если человек решит зарегистрироваться, то мы просто обновляем информацию о пользователе и снимаем гостевой флаг.
Обогащение данных (polling)
Так как контент провайдеров может быть много, то глупо заставлять пользователя ждать, пока ответят все. Некоторые провайдеры тянут резину более чем 30 секунд, за что им должно быть стыдно. Для того, чтобы пользователь видел результаты поиска по мере их поступления, мы интегрировали набирающий популярность механизм обогащения данными (поллинга).
Выглядит это следующим образом:
- Клиент запрашивает некий набор критериев поиска, а в ответ получает не результат, а номер поисковой сессии.
- Веб сервис в воркерах в потоках обращается к контент провайдерам за данными.
- Контент провайдеры по мере работы возвращают искомые данные.
- Клиент запрашивает данные у веб сервиса по ID сессии.
- Если данных нет, то клиента просят прийти позже.
- Если данные есть, то клиенту отправляются данные.
- Если данных больше нет и не будет, клиент извещается об окончании поиска.
Администрирование
Админки в данный момент попросту нет. Мы не позаботились об этом на начальном этапе, а сейчас найти время на этот компонент не получается. Администрируется сейчас все через статистические сообщения, которые периодически валятся в логи, е-mail отчеты и напрямую через MySQL Workbench.
Как сейчас продвигаются дела?
Сейчас все находится в зависшем состоянии. Я не возлагал на все это коммерческих надежд и делал все исключительно Just for fun. Сейчас я практически не уделяю времени всему этому, хотя ошибок еще присутствует довольно много, да и развиваться есть куда.
Если у вас есть вопросы/советы/идеи, буду рад обсудить их в комментария, возможно даже дополню статью интересными моментами, о которых забыл упомянуть. Все грамматические замечания по традиции Хабра прошу писать в личку.
Спасибо что дочитали до конца. А если не дочитали, то все равно спасибо!
PS: Предвидя вопрос, заданный в первом комментарии, отвечу сразу. Приложение называется Mobi Booking.
Автор: surik