История о том, как мы в ОК упрощали процесс создания API

3bcc64585b04162e5d4d87d4b7857cab.jpg

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

Не так давно команда ОК столкнулась с подобной дилеммой: исторически для отображения страниц мы использовали server-side рендеринг на базе GWT (Google Web Toolkit) и RDK (наша внутренняя разработка), так как разработка первой версии ОК началась более 20 лет назад. Такой набор технологий «под капотом» был оптимален в те времена, но сейчас он, мягко говоря, не самый желательный. Поэтому нам было важно перейти на более распространенную библиотеку, а точнее — на React. Это мы и начали делать итеративно, шаг за шагом для каждого раздела сайта.

Меня зовут Александр Косницкий, я разработчик в компании ОК. В этой статье я расскажу, как мы переводили на React раздел «Обсуждения»: с чего начинали, с чем сталкивались и как в результате смогли получить то, что поможет не только нам, но и остальным командам с переводом на новую библиотеку их собственных разделов.

Подробнее о задаче

Исторически в ОК для отображения страниц использовался server-side рендеринг на базе GWT и RDK. Но в долгосрочной перспективе сохранять такой стек было нерационально — сложно найти фронтенд-разработчиков, которые с таким готовы работать. Соответственно, чтобы одновременно и развивать проект, и упростить его администрирование, нам надо было перейти на более популярные и понятные технологии. В нашем случае приоритетом стал React, который де-факто является стандартом индустрии.

Но история с быстрой, бесшовной миграцией на новую технологию — не наш случай. Дело в том, что для перевода GWT и RDK на React нужен API, на который React будет ходить. И этого API к тому моменту у нас не было. Соответственно, чтобы осуществить переход на React, нам предстояло разработать API для каждого GWT/RDK блока, который бы мы хотели перевести.

Первые подводные камни

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

Несмотря на то, что с точки зрения API разницы между мобильной и десктопной версией нет, фактически мы имели два разных проекта, поскольку код на GWT/RDK у двух платформ сильно отличался.

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

1c7252d11afabe69047ac10612365e45.png

Подход, откровенно говоря, сомнительный, как и его «преимущества»:

  • Всегда есть, чем занять Junior-специалистов, которые только пришли в компанию, потому что надо постоянно поддерживать код в консистентном состоянии между двумя API.

  • Команда тестирования держится в тонусе: то, что хорошо работает на mob-версии, может не работать на вебе и наоборот. Даже несмотря на то, что фронтенд-часть на React одинаковая.

  • Estimate по задачам на создание API можно брать в два раза больше, ведь кода будет в два раза больше.

От копипаста к новой реализации

К созданию единого API нас подталкивали еще несколько моментов:

  • все методы и возвращаемые значения — одинаковые;

  • аннотации, которыми помечаются endpoint-ы — одинаковые и взяты из one-desktop;

  • endpoint-ов фактически два, а frontend React-клиент одинаковый для обеих версий.

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

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

89cda773e5038e73dd73bc28b557e02b.png

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

Несколько «сглаживало» ситуацию лишь то, что классы наследовались от одного класса, который уже вынесен в one-desktop библиотеку.

afd6f500bbaafbb08e49742804c55142.png

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

64a35593460247b56b4e6f20babe7497.png

Чтобы все это запустилось, необходимо было сделать на каждой платформе только пару строчек изменений, которые позволили API понимать, что EndpointContext — тоже валидный аргумент для API endpoint-ов, куда просто надо подставлять обычный AppRequestCtx

На этом подготовка к созданию нового метода под наше API фактически была завершена.

От API к OpenAPI 

Когда мы увидели, что такое кроссплатформенное API действительно успешно завелось и работает, появилась идея создавать и поддерживать контракт этих API через наш собственный OpenAPI генератор, используя API-first подход. Подобная реализация потенциально дает много преимуществ.

  • OpenAPI — хорошо известный всем формат. Написанный разработчиками OpenAPI-файл можно будет напрямую добавлять во множество доступных всем инструментов.

  • Не нужно будет вручную писать документацию по API — все будет в понятном виде описано в самом файле. Более того, по файлу также можно генерировать страницу на wiki.

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

    В дальнейшем файл можно даже захостить и включить генерирование кода на клиенте и беке напрямую в CI. Это удобно — если контракт API поменяется, то новые билды сразу начнут падать, пока разработчики не отреагируют должным образом на изменения. Чтобы поменять этот интерфейс, надо поменять файл, что сразу отразится и на бекенде, и на фронтенде. То есть исключаются ситуации, при которых, например, вносятся изменения в интерфейсе API на беке, а на фронтенде об этом ничего не известно. 

  • Тестировщикам сразу понятно, как тестировать API — не нужно вникать в код или опрашивать разработчиков, чтобы узнать, какие есть аргументы. Достаточно импортировать запросы в postman или открыть в swagger, после чего его можно использовать напрямую.

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

85aa92e8170dcffc7f3676ce0f013c27.png

Причем полученная версия уже могла генерировать все нужные для MVP файлы. Это дало возможность приступать к написанию спецификации OpenAPI, с помощью которой можно нажатием одной кнопки получить интерфейс API.

4c07830a71957ece0d61497dc6ff1076.png

При этом также генерируются все нужные бины.

183951105b2d3c1bb5f2abe42adc2751.png

Более того, генератор легко добавляется плагином Gradle в builscript, что существенно упрощает его интеграцию в финальный пайплайн сборки.

2b4eb36f3b15bfb7bb6251848375d7b4.png

От теории к практике

Теперь давайте посмотрим, как именно работает описанный подход.

При изучении кода, приведенного ниже, можно заметить, что описанная спецификация endpoint-а легко превращается в код в discussionsApi интерфейсе, аналогичный тому, который мы видели в примере с dailyphoto

38f41003ba4795be051a3c28fdfbd739.png

Только теперь код еще и использует в аргументах EndpointContext, что делает его доступным к использованию на обеих платформах сразу (ради чего изначально все и затевалось).

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

d9303d5bd1266928054e5b0e3b63ea9b.png

Более того, у нас есть возможность использовать в API даже те классы, которые мы создали сами, или которые пришли из библиотек. 

07ee07be25cb2ad527635043d8294d63.png

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

  • создать объект-«заглушку» в OpenAPI спецификации;

  • добавить маппинг нашего объекта на реальный Java-класс в schemaMappings в сборочном скрипте генератора. 

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

Помимо прочего, без проблем работает и наследование.

8f5645d1f0d358c3ecb36e4c0d535d24.png

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

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

16d72eb4ededa297001c4eb805a978c5.png

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

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

В итоге мы уже готовы к применению API на интересующих платформах. Остается лишь:  

  1. Реализовать в модуле нашего API класс-конфигурацию, который будет создавать и настраивать все бины.

  2. Собрать сборку зависимости и подключить ее к интересующей нас платформе.

  3. Добавить конфигурацию в applicationContext на этой платформе.

  4. Добавить наше API к списку на инициализацию при старте.

ee074fd207ee30a9ebf6010c4506e43d.pngb01b2f0a3b183787a55e82b4f09799fa.png

Планы на будущее

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

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

  • добавление поддержки запуска подобных API-библиотек еще и на forticom-api (нашем модуле с API для мобильных клиентов);

  • улучшение EndpointContext за счет переноса в него еще большего количества методов из реализаций контекстов на конкретных платформах;

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

  • подготовка более подробной документации.

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

Если хотите обсудить наш подход или поделиться своим опытом — добро пожаловать в комментарии!

© Habrahabr.ru