Розробка архітектури нового додатка для пасажирів Über

— Здрастуйте. Скажіть, скільки коштує зробити додаток типу Über?

Менеджер по вхідним заявками нашої компанії отримує дзвінки з таким змістом стабільно раз в тиждень. Розуміти його варто, як правило, так: або клієнт хоче собі настільки ж успішний аналог програми для зв'язку між пасажиром і водієм, або Über для ______ (вписати потрібну галузь).

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

Тепер у нас є аргумент у захист нашої позиції. Розробники Über опублікували в блозі компанії замітку про досвід перенесення програми з однієї архітектури на нову, власну. Це дуже масштабний захід підтверджує, що Uber — далеко не елементарне додаток. Ми не могли пройти повз цього матеріалу і не перевести його.

Стаття може бути корисною не тільки мобільним розробникам, але і менеджерам, які стикаються з такою ситуацією.



Чому ми переробили Über
Ідея Über проста: натисни на кнопку, і тебе повезуть, куди потрібно. Стартувавши як сервіс для замовлення чорних автомобілів преміум-класу, сьогодні Über надає величезний спектр послуг, щодня координуючи мільйони поїздок у сотнях міст. Щоб відповідати реаліям 2017 року, нам довелося заново розробляти всю архітектуру програми.

Але з чого ж почати? З того ж, з чого ми починали в 2009 році: з нуля. Ми вирішили повністю переписати наш додаток і переробити його дизайн. Відмова від напрацьованої програмної бази і дизайнерських рішень дав нам свободу дій там, де в іншому випадку довелося б шукати компроміси. На виході ми отримали абсолютно новий додаток, вилизане до блиску і дає всі переваги своєї нової архітектури як користувачам iOS, так і користувачів Android. Ця стаття розповідає про те, як ми створили новий архітектурний патерн під назвою Riblets і як з його допомогою ми досягли поставлених цілей.



Мотивація і цілі
У той час, поки основною ідеєю Über залишається надання зв'язку між водіями і пасажирами, наш продукт виріс в щось набагато більше, і архітектура колишнього додатки була вже не в змозі справлятися з підвищеними вимогами. Додавання нових можливостей у додаток рік від року ставало все важче. Такі доповнення, як UberPOOL, заплановані поїздки і фото автомобілів, що тільки ускладнювали роботу. Ризик того, що якась частина програми перестане працювати після внесення чергової зміни, збільшувався з кожним днем разом з його розміром, через що будь-який експеримент міг виявитися причиною довгої налагодження.

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

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

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

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

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

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

Хоча в окремих випадках розробка проходить індивідуально для кожної платформи (наприклад, реалізація інтерфейсу), у обох платформ є багато спільного:

  • основна архітектура;
  • назви класів;
  • взаємозв'язок між модулями бізнес-логіки;
  • поділ бізнес-логіки;
  • Plugin points (назви, existence, структура тощо);
  • ланцюжка реактивного програмування;
  • уніфіковані компоненти.
Щоб максимально використовувати ці загальні риси, наша нова архітектура вимагає чіткої організації та розподілу між бізнес-логікою, логікою подання, потоками даних і маршрутизацією. Подібна архітектура допомагає уникнути запутанностей, спростити тестування і, отже, збільшити продуктивність розробки і надійність кінцевого продукту.

Від MVC до Riblets
Визначившись із завданнями, ми вирішили з'ясувати, як можна поліпшити нашу стару архітектуру і зайнялися дослідженням можливостей. Кодова база, яку ми успадкували від старої версії Uber, була заснована на архітектурному паттерне MVC. Ми вивчили інші патерни, наприклад, VIPER, який ми частково використовували при створенні Riblets. Основна інновація Riblets полягає в маршрутизації за допомогою бізнес-логіки замість логіки подання. Якщо ви не знайомі з MVC і Riblets, прочитайте кілька статей, присвячених сучасним архітектурним паттернам для iOS (наприклад, цю) Так вам буде простіше зрозуміти переваги і недоліки адаптації цих патернів для Uber.

З чого ми почали: MVC (Model-View-Controller)

Перше додаток для пасажирів було написано невеликою групою розробників майже чотири роки тому. В той момент використання MVC здавалося виправданим. Коли команда розробників зросла до кількох сотень людей, ми зіткнулися з тим фактом, що MVC не може рости разом з нами. Основних причин було дві:

  1. зростаюча архітектура MVC часто стикається з проблемою під назвою massive view controller. Наприклад, RequestViewController, що починався з 300 рядків коду, в поточному стані містить більше 3000 рядків з-за великої кількості покладений на нього обов'язків: бізнес-логіка, маніпуляція даними, перевірка даних, мережева логіка, логіка маршрутизації і т. д. Читати і модифікувати його стало дуже складно;
  2. архітектура MVC надає вельми крихкий процес оновлення коду і ускладнює тестування. В процесі розробки нових доповнень ми дуже багато експериментуємо. Всі наші експерименти зводяться до роботи з операторами if-else. Кожен раз, коли трапляється клас з великою кількістю функцій, оператори if-else навалюються один на одного, зводячи до нуля можливість тестування. Додатково до цього, з ростом внутрішніх шматків коду, таких, як RequestViewController і TripViewController, створення оновлення для програми перетворилося на дуже вразливий і чутливий процес. Уявіть собі, яке це — для кожного можливого зміни тестувати всі можливі комбінації операторів if-else, вкладених один в одного.
Раз вже ми хотіли продовжувати експерименти з метою подальшого розвитку програми та зростання бізнесу Uber, довелося визнати, що дана архітектура вичерпала себе.

На шляху: VIPER

В процесі пошуку альтернативи MVC ми знайшли VIPER, який є прикладом застосування чистої архітектури при розробці iOS-додатків. VIPER володіє рядом ключових переваг порівняно з MVC:

  1. він пропонує набагато більше абстракції. Presenter містить логіку, яка з'єднує бізнес-логіку з логікою подання. Interactor займається маніпуляціями з даними, а також їх перевіркою. Це включає в себе запити до бэкенду для маніпуляцій з станом, наприклад для входу або замовлення поїздки. І, нарешті, Router ініціалізує переходи (один з них — як перехід з домашньої сторінки на сторінку підтвердження замовлення);
  2. у випадку з VIPER Presenter і Interactor є Plain Old Objects, тому ми можемо використовувати звичайне unit-тестування.
Але ми також знайшли у VIPER кілька недоліків:
  1. його конструкція, спеціалізована для iOS, означала, що нам доведеться шукати компроміси для Android;
  2. view-driven-логіка означає, що програми управляються компонентами вистави, і все додаток прив'язане до дерева подання;
  3. Бізнес-логіка, виконувана Interactor, який повинен керувати станом додатки, завжди повинна пройти через Presenter, в якому вона втрачається;
  4. при тісно пов'язаних один з одним деревами подання та логіки реалізація елемента, який містить тільки один тип логіки, стає дуже складною.
Будучи явно краще, ніж MVC, VIPER не може повністю задовольнити потребу Über в розширюваної платформи з чіткою модульність. Тому ми повернулися до дошки для малювання, щоб постаратися розробити архітектурний патерн з перевагами VIPER і без його недоліків. Результатом став Riblets.

Riblets: архітектура програми для пасажирів Über
У нашому новому архітектурному паттерне логіка розбита на невеликі незалежно досліджувані шматки. Кожен зі шматків має одне-єдине призначення згідно з принципом єдиною відповідальності. Ми використовуємо Riblets у вигляді цих модульних шматків, і структура всього програми являє собою дерево Riblets.

Riblets та їх компоненти

З Riblets ми розподілили відповідальності з шести різних компонентів, щоб ще сильніше абстрагувати бізнес-логіки від логіки подання:



Чим відрізняється Riblets від VIPER і MVC? Прокладка маршруту проводиться бізнес-логікою замість логіки подання. Це означає, що програма управляється потоком інформації та прийнятих рішень, а не зовнішнім виглядом. Далеко не кожен шматок бізнес-логіки в Über має відношення до чого-те, що бачить користувач. Замість використання бізнес-логіки в ViewController в MVC або маніпулювання станами програми через Presenter в VIPER ми можемо зробити окремий Riblet для кожного шматка бізнес-логіки, створюючи локальні групи, з якими набагато простіше працювати. Крім цього ми розробили патерн Riblet таким чином, що він не залежить від платформи. Останнє дозволяє об'єднати розробку для iOS і Android.

Кожен Riblet включає в себе Router, Interactor і Builder з його власним Component (звідси і назва) і, при необхідності, Presenters і Views. Router і Interactor займаються бізнес-логікою, в той час, як Presenter View і займаються логікою подання.

Давайте розберемо, чим займається кожен елемент Riblet, використовуючи для прикладу Production Select Rible.


Вибір тарифу у новому додатку Über

Builder
Builder встановлює всі первинні елементи Riblet і залежності між ними. У Product Selection Riblet цей елемент встановлює залежність потоку даних для необхідного міста.

Component
Component отримує і встановлює залежності Riblet. Це включає в себе служби, потоки даних, і все інше, що не є первинним елементом Riblet. Product Selection Component отримує і встановлює залежність від міського потоку, прикріплює її до відповідних мережевих подій і впроваджує в Interactor.

Routers
Routers формують дерево додатки, прикріплюючи і открепляя дочірні Riblets. Ці рішення їм передає Interactor. Також Routers керують життєвим циклом Interactor, включаючи і вимикаючи при певних станах програми.

Routers містять два шматки бізнес-логіки:

  1. Helper methods — для приєднання і від'єднання Routers.
  2. State-switching — логіка для визначення стану дочірніх модулів.
Product Selection Riblet не має дочірніх Riblets. Router його батьківського Riblet, Confirmation Riblet, несе відповідальність за прикріплення Product Selection Router і додавання його View Views-ієрархію. Потім, коли продукт вибраний, Product Selection Router деактивує свій Interactor.

Interactors
Interactors виконують бізнес-логіку. Це включає:

  • надсилання сервісних викликів для запуску якої-небудь дії, наприклад, замовлення поїздки;
  • надсилання сервісних викликів для отримання даних;
  • визначення стану для перемикання. Приміром, якщо кореневий Interactor бачить, що токен авторизації користувача, він посилає свого Router запит на перехід у стан Welcome.


Product Selection Interactor бере міський потік, що містить дані, що включають пропозиції служб цього міста, інформацію про ціни, приблизний час поїздки і фотографії автомобілів, і передає цю інформацію в Presenter. Якщо користувач переходить з uberPOOL в uberX, Interactor отримує цю інформацію від Presenter, після чого він збирає всі відповідні дані і передає їх назад в View, щоб він відобразив автомобілі uberX і приблизний час прибуття. Якщо коротко, Interactor виконує всю бізнес-логіку, яку потім відображає View.

View(Controller)
Views конфігурують і оновлюють користувальницький інтерфейс, включаючи створення і розташування окремих елементів, взаємодія з користувачем, заповнення елементів інтерфейсу даними і анімацією. View у Product Selection Riblet відображає об'єкти, які йому передає Presenter (конфігурації поїздки, ціни, приблизний час прибуття, зображення автомобіля на карті) і повертає дії користувача (тобто вибір продукту).

Presenter
Presenter управляє комунікацією між Interactors і Views. Від Interactors до Views, Presenter передає бізнес-моделі об'єктів, які відображає View. У випадку з Product Selection Riblet це дані про ціни і зображення автомобілів. Також завданням Presenter є перетворення дій користувача (таких, як натискання на кнопку вибору продукту) в команди, які потім передаються в Interactor.

Збираємо всі разом
Кожен Riblet містить тільки одну пару Router і Interactor, але в ньому може бути кілька частин подання. Riblet, відповідає виключно за бізнес-логіку і не має елементів користувальницького інтерфейсу, не містить view-частини.

Riblet може бути:

  • single-view (один Presenter і один View);
  • multi-view (або один Presenter і кілька View, або кілька Presenter View і);
  • viewless (без Presenter і View).
Це дозволяє деревам бізнес-логіки бути глибокими і відрізнятися від більш плоских дерев уявлення, завдяки чому перемикання між екранами стає простіше.

Наприклад, Ride Riblet є viewless. Його завдання — перевірка того, чи є у користувача активна поїздка. Якщо це так, він підключає Trip Riblet, який показує маршрут на карті. Якщо ні, то підключається Request Riblet, що показує екран, на якому користувач може замовити поїздку. Основним завданням Riblets, не містять логіку подання таких як Ride Riblet), є ізоляція бізнес-логіки, яка керує нашими додатками, завдяки чому зберігається модульна структура нашої нової архітектури.

Як створити додаток з Riblets
Riblets об'єднуються в дерево додатки і їм необхідно підтримувати зв'язок один з одним для того, щоб оновлювати інформацію або провести користувача до наступного кроку замовлення поїздки. Перед тим, як розповісти, яким чином вони зв'язуються один з одним, давайте розберемося, як дані передаються всередині окремо взятого Riblet.

Потік даних всередині Riblet

Interactor зберігає бізнес-логіку, яка управляє додатком і відповідає сфері повноважень цього Interactor. Цей елемент робить запити до служб (service calls), щоб отримати необхідні дані.

Дані в новій архітектурі завжди передаються тільки в одному напрямку. Вони переходять від Service до Model Stream, а потім до Interactor. Interactors, планувальники подій і push-повідомлення з інтернету можуть робити запити до Services для внесення змін до Model Stream.

Model Stream створює иммутабельные моделі. Це створює вимоги, згідно з якими для того, щоб змінити стан додатки, Interactor класи повинні використовувати сервісний шар.



Приклади потоків:

  • від backend сервісу до View: запит до служб, наприклад, запит статусу, дані про якого беруться з backend. Дані поміщаються в незмінний Model Stream. Interactor, слухає цей потік, зауважує нові дані і передає їх у Presenter. Presenter перетворює дані і передає їх до View;
  • від View до backend: користувач натискає на кнопку, наприклад, «Вхід», і View передає це дія в Presenter. Presenter викликає метод sign-in в Interactor, в результаті чого створюється запит до сервісів для входу в програму. Сервіс відправляє повернутий токен в потік, а Interactor, слухає цей потік, перемикається на Home Riblet.


Взаємодія між Riblets





Коли Interactor приймає якесь рішення, йому може знадобитися проінформувати іншого Riblet про це і переслати дані. Для цього Interactor викликає інтерфейс, який погоджує його з Interactor іншого Riblet.

У разі, якщо зв'язок йде вгору по дереву до Interactor батьківського Riblet, інтерфейс визначається як слухач (Listener). Слухач майже завжди встановлюється Interactor'ом батьківського Riblet. Якщо зв'язок йде вниз до дочірнього Riblet, інтерфейс повинен бути визначений як делегат, і встановлюється Interactor'ом дочірнього Riblet. Делегати використовуються тільки для прямої синхронного зв'язку між елементами, наприклад батьківського Interactor з дочірнім.

У разі низхідній зв'язку батьківський Riblet може направити до Interactor дочірнього Riblet вами потік даних, батьківський Interactor може потім відправляти дані через цей потік замість інтерфейсу-делегата. У більшості випадків низхідній зв'язку для передачі даних подібний метод повинен бути кращим.

Наприклад, коли гіпотетичний ProductSelectionInteractor розуміє, що продукт обраний, він звертається до свого слухача, встановленим ConfirmationInteractor'ом, і передає йому view-ідентифікатор (ID) обраного автомобіля. ConfirmationInteractor зберігає цей ID, щоб потім відправити його у запиті до сервісів і відправляє в свій Router запит на «відключення» ProductSelection Riblet.

Структуруючи подібним чином потік даних всередині і між Riblets, ми можемо бути впевнені в тому, що потрібні дані в потрібний момент будуть відображені на потрібному екрані. Так як дерево Riblets базується на бізнес-логікою, ми можемо встановлювати зв'язок на рівні бізнес-логіки замість логіки подання. Це має набагато більший сенс і допомагає зберегти ізоляцію коду, захищаючи розробку програми від непотрібних складнощів.

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

Як ми домоглися збільшення надійності та доступності для кінцевого користувача

Riblets мають чітко розмежовані відповідальності, тому тестування стало набагато простіше. Кожен Riblet може бути протестований незалежно від інших. З такими можливостями ми можемо бути впевнені в тому, що у нашого додатка не з'являться баги після чергового оновлення. Завдяки тому, що кожен Riblet несе одну-єдину відповідальність, ми змогли легко розділити Riblets на основний (необхідний для входу в програму і замовлення поїздки в uberPOOL і uberX) і опціональний код. Пред'являючи високі вимоги до перевірок основного коду, ми можемо бути впевнені в тому, що основні функції програми будуть працювати з максимальною безвідмовністю.

Ми також зробили можливим глобальний відкат основних функцій програми до гарантовано робочого стану. Весь опціональний код відзначений прапорами master feature, які можуть бути вимкнені, якщо частини коду містять помилки або працюють нестабільно. При найгіршому варіанті розвитку подій ми можемо відключити абсолютно весь опціональний код, залишивши тільки основу. Враховуючи наші суворі вимоги до основного коду, ми можемо бути впевнені, що воно буде працювати завжди.

Як ми створили правильні умови для майбутніх розробок

Завдяки вузькій спеціалізації кожного Riblet ми змогли провести чітку межу між бізнес-логікою та логікою подання. Це зможе запобігти надмірне розростання кодової бази й збереже її простоту. Так як нова архітектура є кроссплатформної, iOS та Android-розробники можуть легко розуміти одне одного, вчитися на помилках товаришів і спільно працювати над подальшим розвитком Über. Експерименти будуть менше впливати на працездатність ядра програми, так як Riblets допомагають нам відокремити опціональний код від основного. Ми зможемо відчувати нові додатки, створені у вигляді плагінів, не побоюючись, що вони можуть випадково вивести з ладу весь додаток.

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

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

Завдяки новій архітектурі у нас з'явилася величезна кількість можливостей для подальшого розвитку. Якщо чесно, ми провели пару місяців за складанням прототипів, щоб переконатися, що ми все зробили правильно. Тепер ми повністю впевнені в тому, що у нас є архітектура, з якої ми зможемо досягти дуже багато чого. Якщо така робота вам подобається, ви можете стати частиною нашої історії і поліпшити думку про Uber, беручи участь в розробках як iOS або Android-розробника.
Джерело: Хабрахабр

0 коментарів

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