[Перевод] Agile API — возможно ли?
Для начала я должен немного подробнее рассказать о Badoo, чтобы вы понимали, кто работает с нашим API и почему оно так часто меняется.
Наше внутреннее API и его использование
Наш Badoo API (протокол обмена сообщениями) представляет собой набор структур данных (сообщений) и значений (enums), которыми обмениваются клиенты и сервер. Структура протокола у нас задаётся на основе определений Google protobuf и хранится в отдельном репозитории git. На основе этих определений генерируются классы моделей для различных платформ.
Он используется для шести платформ — сервера и пяти клиентов: Android, iOS, Windows Phone, Mobile Web и Desktop Web. Кроме того, тот же самый API используется в нескольких самостоятельных приложениях на каждой платформе. Чтобы все эти приложения и сервер могли обмениваться требуемой информацией, наш API «вырос» достаточно большим. Немного цифр:
- 450 сообщений, 2665 полей.
- 135 enum«ов, 2096 значений.
- 125 флагов фич, которыми можно управлять на стороне сервера.
- 165 флагов клиентского поведения, которые сообщают серверу особенности поведения клиента и поддерживаемого клиентом протокола.
Когда это нужно сделать? Вчера!
В Badoo мы стараемся реализовывать новые функции как можно быстрее. Логика проста: чем скорее появится версия с новыми возможностями, тем быстрее пользователи смогут ими воспользоваться. Кроме того, параллельно мы проводим A/B-тестирование, но в этой статье мы не станем подробно останавливаться на нем.
Время реализации фичи — от идеи до релиза — иногда всего лишь неделя, включая написание требований, изменение API и технической документации, имплементация и релиз новой версии. В среднем же на все уходит около месяца. Однако это не означает, что мы добавляем по одной фиче в приложение в месяц. Мы работаем параллельно над множеством новых фич.
Для того, чтобы быть способными на такие подвиги, нам пришлось разработать процесс, который позволяет нам двигаться с нужной скоростью.
С какой стороны подойти?
К примеру, владелец продукта предлагает новую идею и просит разработчиков API расширить протокол, чтобы реализовать новые возможности на стороне клиентов.
Прежде всего необходимо понимать, что над реализацией новой функции работает множество людей:
- Product Owner;
- дизайнеры;
- разработчики серверных решений;
- разработчики API
- разработчики клиентских приложений для разных платформ;
- QA;
- аналитики данных.
Как можно гарантировать, что все они понимают друг друга и говорят на одном языке? Нам нужен документ с описанием требований к функциональности (PRD).Обычно такой документ готовит Product Owner. Он создает вики-страницу с перечнем требований, вариантами использования, описанием флоу, эскизами дизайна и т. д.
На основе PRD мы можем приступать к планированию и реализации необходимых изменений в API.
Тут опять не всё так просто.
Подходы к проектированию протокола
Существует много подходов к «распределению обязанностей» между сервером и клиентом. Подходы варьируются от «вся логика реализована на сервере» до «вся логика реализована в клиенте». Обсудим «за» и «против» каждого подхода:
Вариант 1. Вся логика реализована на сервере (клиент работает как View из шаблона MVC).
Плюсы:
- Новые функциональные возможности для всех платформ достаточно реализовать только один раз — на сервере.
- Можно обновить только серверную логику и лексемы, нет нужды вносить изменения в клиентские приложения и готовить новый релиз (очень большой плюс, когда речь идет о нативных приложениях).
Минусы:
- Более сложный протокол (часто фича требует несколько последовательных действий, которые легко реализовать на клиенте, а добавление этих шагов в протокол сильно все усложняет).
- Если что-то работает по-разному на разных клиентских приложениях, необходимо иметь на сервере отдельную реализацию функционала для каждого клиентского приложения и каждой поддерживаемой версии приложения.
- Может негативно повлиять на удобство использования приложения через медленное или нестабильное соединение.
- Если бизнес-логика реализована на стороне сервера, некоторые функции будет очень сложно или вовсе невозможно реализовать при отсутствии соединения с сервером.
Вариант 2. Вся логика реализована в клиенте — клиентское приложение содержит всю логику и использует сервер как источник данных (характерно для большинства публичных API).
Плюсы:
- Количество запросов к серверу меньше, пользователю приходится меньше ждать ответов.
- Лучше работает в автономном режиме и через медленную или нестабильную сеть.
- Значительно упрощается кэширование.
- При необходимости гораздо проще реализовать различающееся поведение для разных платформ и версий клиентов.
- Проще взаимодействие с сервером — команды могут работать без оглядки друг на друга.
Минусы:
- Занимает больше времени. Всю логику необходимо реализовать на каждом из клиентов, а не один раз на сервере.
- Для реализации даже самых незначительных изменений необходимо релизить каждое клиентское приложение.
- Выше вероятность ошибок в коде, поскольку у каждого приложения отдельная реализация логики.
С одной стороны, первый подход позволяет реализовать бизнес-логику только один раз на сервере, и затем она будет использоваться на всех клиентах. С другой стороны, для разных платформ характерны свои особенности, своя структура лексем, разный набор фич, а сами фичи нередко реализуются в разное время. В большинстве случаем проще делать протокол более ориентированным на данные, чтобы клиентские приложения получили некоторую свободу и могли работать по-своему. Но, как всегда, единственно верного решения нет, поэтому мы постоянно балансируем между этими двумя подходами.
Техническая документация
Несколько лет назад, когда наша компания была меньше, только два клиента (Android и iOS) использовали протокол. Разработчиков было мало, все рабочие моменты мы обсуждали устно, поэтому документация на протокол содержала лишь описание общей логики в комментариях для определений protobuf. Обычно это выглядело так:
Затем появились еще три клиентские платформы: Windows Phone, Mobile Web и Desktop Web, да количество разработчиков в Android и iOS командах выросло в три раза. Устное обсуждение стало обходиться все дороже, и мы решили, что настало время тщательно все документировать. Теперь эта документация включает намного больше, чем просто комментарии о полях. Например, мы добавляем краткое описание фичи, sequence-диаграммы, скриншоты и примеры сообщений. В простейшем случае документация может выглядеть так:
Разработчики приложений и QA для всех шести платформах, используют эту документацию и PRD в качестве основных источников знания.
Эта документация помогает не только реализации новых функций, но и при значительном редизайне и рефакторинге приложений, когда требуется знать как работает уже имплементированный функционал.
Теперь мы можем с легкостью сказать разработчикам «RTFM», что экономит нам массу времени. Кроме того, такой подход помогает новичкам понять, как все устроено, и быстро включиться в процесс разработки.
Сервируем документацию
Мы готовим техническую документацию в формате reStructuredText и храним ее в репозитории git вместе с определениями протокола, а с помощью Sphinx мы генерируем HTML-версию, которая доступна во внутренней сети работникам компании.
Документация разделена на ряд основных разделов, посвященных различным аспектам реализации протокола и другим вопросам:
- Протокол — документация, созданная на основе комментариев для определений protobuf. Функции продукта — техническая документация по вопросам реализации функций. (диаграмма конвейера и т. д.).
- Общее — документы о протоколе и флоу, не относящаяся к конкретным фичам продукта.
- Особенности приложений — поскольку у нас несколько различных приложений, в этом разделе описаны различия между ними. Как уже говорилось выше, протокол используется общий.
- Статистика — общее описание протокола и процессов, связанных со сбором статистики и информации о производительности приложений.
- Нотификации — документация о различных типах уведомлений, которые могу приходить нашим пользователям.
- Архитектура и инфраструктура — структура нижнего уровня для протокола, двоичные форматы протокола, фреймворк для A/B-тестирования и т. д.
Итак, мы внесли изменения в API. Что дальше?
После внесения изменений в протокол и документация на основе PRD, и PRD и API проходят две стадии ревью. Сначала внутри команды, отвечающей за протокол, затем у разработчиков приложений тех платформ, где будет реализована эта фича.
На этом этапе мы получаем обратную связь по следующим вопросам:
- Достаточность — достаточно ли данных изменений API для реализации фичи на каждой платформе.
- Совместимость — совместимы ли изменения с кодом приложений на платформах. Может быть можно немного подправить протокол и сэкономить одной-двум платформам кучу времени и ресурсов?
- Понятность
После обсуждения и утверждения всех изменений серверные и клиентские команды могу приступать к реализации фичи.
Закончить на данном этапе было бы слишком хорошо. Часто у ProductOwner«а уже есть дальнейшие планы по улучшению данной фичи, но при этом он все же хочет выпустить приложение сейчас в текущем виде. Или еще интереснее. Иногда нас просят внести изменения для одной платформы, а для остальных оставить все как есть.
Фича как ребенок, она растет и развивается
Фичи развиваются. Мы проводим A/B-тесты и (или) анализируем обратную связь после выпуска новых фич. Иногда анализ показывает, что фича нуждается в доработке. Тогда Product Owner’ы вносят изменения в PRD. И тут возникает «проблема». Теперь PRD не соответствует формату протокола и документации. Более того, может получиться так, что для одной платформы изменения уже внесены, а другая команда только приступает к работе. Чтобы предотвратить возможные противоречия, мы используем версионирование PRD. К примеру, для одной платформы фича может быть реализована в соответствии с версией R3. Через некоторое время Product Owner решает доработать новый функционал и обновляет PRD до версии R5. И мы должны обновить протокол и техническую документацию с учетом новой версии PRD.
Для мониторинга обновлений PRD мы используем историю изменений в Confluence (вики от компании Atlassian). В технической документации на протокол мы добавляем ссылки на конкретную версию PRD, просто указывая ? pageVersion=3 в адресе вики-страницы, или берем ссылку из истории изменений. Благодаря этому каждый разработчик всегда знает, на основе какой версии или части PRD реализована та или иная функция.
Все изменения в PRD рассматриваются как новый функционал. Product Owner«ы накапливают изменения (R1, R2…) до тех пор, пока не решат, что настала пора отправить их в разработку. Они готовят задание для разработчиков API с описанием требуемых изменений в протоколе, а затем такие же задания получают все команды разработчиков, отвечающие за разные платформы. Когда для фичи готов следующий набор изменений, создается еще один тикет на доработку API, затем аналогичным образом реализуются изменения для всех платформ:
Получив перечень изменений в PRD, мы возвращаемся в начало этого процесса, то есть вносим изменения в протокол и так далее. Конечно, это осложняет нам жизнь, поскольку никто не отменял необходимость поддержки реализованных ранее функций и клиентских приложений, которые были основаны на версии R3. Для решения этой проблемы мы используем несколько инструментов управления изменениями в протоколе.
Управление изменениями в протоколе
В предыдущем разделе мы обсудили контроль версий PRD. Чтобы реализовать эти изменения в API, мы должны рассмотреть варианты управления версиями протокола. Для простоты можно сказать, что существует три варианта (уровня) со своими преимуществами и недостатками.
Уровень протокола
Такой подход широко используется для медленно меняющихся публичных API. Когда выходит новая версия протокола, все клиенты должны начать использовать ее вместо старой. Мы не можем использовать этот вариант, поскольку у нас набор фич и время их реализации очень сильно отличаются на разных платформах. Например, у нас есть несколько версий протокола:
- V1. Поддерживает функции A, B, C.
- V2. Поддерживает функции B», C и D, где B» — это обновленная функция B (требующая другой последовательности команд).
Поэтому, если в приложении нужно реализовать фичу D, придется также обновить фичу B до версии B», хотя, возможно, сейчас это не требуется.
Мы в Badoo никогда не применяли такой подход к контролю версий. В нашем случае больше подходят два следующих варианта.
Контроль версий на основе сообщений
При этом подходе после внесения изменений в функцию создается новое сообщение (структура данных в protobuf) с новым набором полей. Такой подход хорошо работает, если требования меняются значительно.
Например, в компании Badoo у каждого пользователя есть альбомы. Раньше пользователи могли создавать собственные альбомы и помещать в них фотографии:
AddPhotoToAlbumV1 {
required string album_id = 1;
required string photo_id = 2;
}
Затем Product Owner решил, что достаточно будет трех заранее определенных типов альбомов: my photos (мои фотографии), other photos (прочие фотографии) и private photos (личные фотографии). Чтобы клиенты могли различать эти три типа, нужно было добавить enum; соответственно, следующая версия сообщения будет выглядеть следующим образом:
AddPhotoToAlbumV2 {
required AlbumType album_type = 1;
required string photo_id = 2;
}
Такой подход иногда вполне оправдан, но нужно действовать осторожно. Если изменение не будет быстро реализовано на всех платформах, придётся поддерживать (добавляя новые изменения) как старые, так и для новые версии, то есть хаос будет нарастать.
Уровень полей/значений
По возможности мы используем то же самое сообщение или перечисление, удаляя некоторые поля/значения или добавляя новые. Это, пожалуй, самый распространенный подход в нашей практике.
Пример:
AddPhotoToAlbum {
optional string album_id = 1 [deprecated=true];
optional string photo_id = 2;
optional AlbumType album_type = 3;
}
В данном случае клиентские приложения продолжают использовать старое сообщение, а новые версии приложений могут использовать album_type вместо album_id.
Кстати, мы всегда используем необязательные поля. Это позволяет удалять поля в случае необходимости (разработчики Google пришли к такому же выводу).
Поддержка изменений протокола
Как уже говорилось ранее, наше API используется сервером и пятью клиентскими платформами. Новые версии наших клиентских приложений выпускаются каждую неделю (всего около 20 версий приложений в месяц, которые могут работать по-разному и использовать разные части протокола), поэтому мы не можем просто создавать новую версию протокола для каждой новой версии приложения. Такой подход к управлению версиями протокола потребует, чтобы сервер поддерживал тысячи различных комбинаций работы приложений. Это решение далеко от идеального.
Поэтому мы остановились на варианте, когда каждое приложение сразу передает серверу сведения о том, какие версии протокола оно поддерживает. В этом случае сервер может взаимодействовать с любым клиентским приложением, просто полагаясь на список поддерживаемых фич, предоставленный самим приложением.
Например, недавно мы реализовали функцию «What«s New» (Что нового). Таким образом мы информируем наших пользователей о новых функциях в приложении. Приложения, поддерживающие эту функцию, отправляют серверу флаг SUPPORTS_WHATS_NEW. В результате сервер знает, что клиенту можно отправлять сообщения о новых функциях и они будут нормально отображаться.
Как поддерживать порядок в API?
Если это общедоступный API, то обычно определяется конкретная дата, после наступления которой старая часть перестает работать. Для Badoo это практически невозможно, поскольку нам важнее реализовать новые функции, чем убирать поддержку старых. В подобной ситуации мы следуем процедуре, состоящей из трех этапов.
На первом этапе, как только мы окончательно определились, что часть протокола должна быть удалена, она помечается как «устаревшая», и разработчики приложений для всех клиентских платформ получают задачу на удаление соответствующего кода.
На втором этапе устаревшая часть протокола должно быть удалена из кода всех клиентов, но на сервере нельзя удалять этот код ещё достаточно долго — не все пользователи обновляют свои приложения быстро.
На последнем этапе, когда из всех клиентских приложений удален устаревший код, и ни одна из версий, в которой используется эта часть протокола, больше не используется, она может быть удалена из серверного кода и из API.
Общение
В этой статье мы описали несколько технических и организационных подходов, которые мы позаимствовали или создали сами. Однако мы совсем не затронули вопросы общения. А ведь общение составляет 80% нашей работы. Часто необходимо обсудить саму фичу и возможные пути реализации с большим количеством людей, прежде чем удается понять как эта фича может быть реализована на уровне протокола.
В основе любого успешного проекта лежит упорная работа слаженной команды. К счастью, большинство разработчиков поддерживают нас, поскольку хорошо знают, как сложно работать с решениями для различных платформ без стандартизации.
Мы поняли, что хорошо документированный API также помогает людям, которые не являются разработчиками, понять сам API и процесс его разработки. Тестировщики обращаются к документации в ходе тестирования, а Product Owner«ы используют ее для того, чтобы продумать варианты решения поставленных задач с минимальными изменениями в протоколе (да, у нас такие крутые Product Owner«ы!).
Заключение
При разработке гибких API и связанных с ним процессов необходимо быть терпеливыми и прагматичными. Протокол и процесс должны работать с различными комбинациями команд, версий ПО и платформ, устаревшими версиями приложения, а также учитывать многие другие факторы. Тем не менее, это очень интересная задача, по которой на данный момент опубликовано крайне мало информации. Поэтому мы были очень рады поделиться нашими наработками в данном направлении. Спасибо за внимание!