Agile API — чи можливо?

Безліч статей і книг присвячено тому, як правильно проектувати API, але навряд чи хтось зачіпав тему постійно мінливих (гнучких) API. Динамічно розвивається компанія часто випускає кілька релізів на тиждень, а іноді і в день. При цьому для додавання нових функцій необхідно постійно вносити зміни в існуюче API. У цій статті ми розповімо про те, як ми в Badoo вирішуємо це завдання, які підходи та ідеї ми використовуємо у своїй роботі.

Для початку я повинен трохи докладніше розповісти про 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, потім аналогічним чином реалізуються зміни для всіх платформ:

Кроки по зміні 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. В результаті сервер знає, що клієнту можна відправляти повідомлення про нові функції і вони будуть відображатися.

image

Як підтримувати порядок в API?
Якщо це загальнодоступний API, то зазвичай визначається конкретна дата, після настання якої стара частина перестає працювати. Для Badoo це практично неможливо, оскільки нам важливіше реалізувати нові функції, ніж прибирати підтримку старих. У подібній ситуації ми йдемо за процедурою, що складається з трьох етапів.

На першому етапі, як тільки ми остаточно визначилися, що частина протоколу повинна бути видалена, вона позначається як «застаріла», і розробники додатків для всіх клієнтських платформ отримують завдання на видалення відповідного коду.

На другому етапі застаріла частина протоколу повинна бути видалена з коду всіх клієнтів, але на сервері можна видаляти цей код ще досить довго — не всі користувачі оновлюють свої програми швидко.

На останньому етапі, коли з всіх клієнтських додатків видалений застарілий код, і ні одна з версій, в якому використовується ця частина протоколу, більше не використовується, вона може бути вилучена з серверного коду і з API.

Спілкування
У цій статті ми описали декілька технічних та організаційних підходів, які ми запозичили або створили самі. Проте ми зовсім не торкнулися питання спілкування. А адже спілкування становить 80 % нашої роботи. Часто необхідно обговорити саму фічу і можливі шляхи реалізації з великою кількістю людей, перш ніж вдається зрозуміти як ця фіча може бути реалізована на рівні протоколу.

В основі будь-якого успішного проекту лежить наполеглива робота злагодженої команди. На щастя, більшість розробників підтримують нас, оскільки добре знають, як складно працювати з рішеннями для різних платформ без стандартизації.

Ми зрозуміли, що добре документований API також допомагає людям, які не є розробниками, зрозуміти сам API і процес його розробки. Тестувальники звертаються до документації в ході тестування, а Product Owner'и використовують її для того, щоб продумати варіанти рішення поставлених задач з мінімальними змінами в протоколі (так, у нас такі круті Product Owner'и!).

Висновок
При розробці гнучких API і пов'язаних з ним процесів необхідно бути терплячими і прагматичними. Протокол і процес повинні працювати з різними комбінаціями команд, версій і платформ, застарілими версіями програми, а також враховувати багато інших чинників. Тим не менш, це дуже цікаве завдання, за якою на даний момент опубліковано вкрай мало інформації. Тому ми були дуже раді поділитися нашими напрацюваннями в даному напрямку. Спасибі за увагу!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.