top of page

Побудова модульної архітектури проєкту на Android. Досвід Headway



Побудова модульної архітектури — поширений підхід для розробки масштабованих і підтримуваних проєктів для Android. Який спосіб розділення на модулі обрати? Яка їхня оптимальна кількість? Чи варто робити модуляризацію, коли проєкт на стадії MVP? Владислав Козир, Android Engineer у Headway поділився кейсом впровадження модульної архітектури для учасників комʼюніті мобільної розробки. Він розповів про теоретичні та практичні аспекти, підхід, який рекомендує Google, згадав, з якими проблемами стикнулася його команда під час імплементації рішення та як їх долала.




Модульна архітектура: плюси, мінуси, підводні камені


Як виглядає типовий флоу розробки більшості продуктових команд: отримавши дизайн певної фічі, команда береться до розробки. Якщо задача досить складна, може зʼявитися проміжний етап її проєктування всередині команди. В ідеальному світі після закінчення розробки команда передає ресурси на локалізацію, «релізиться» та починає A/B тестування. Отримавши результати, приймає рішення на основі аналітики. І зрештою має почистити конфіги та прибрати зайві івенти з аналітики.


Flow-rozrobky-prodyktovoi-komandy


Подібний флоу мала наша команда, але зі зростанням ми стикнулися з певними проблемами. Одним із найочевидніших рішень була модуляризація. Постало питання: чи підійде це нашому кейсу, та чи буде результат вартим зусиль?


shema-moduliv-ta-zvyzkiv-mij-nymy

Так виглядає умовна схема модулів та звʼязків між ними у нашому хедлайнері — застосунку Headway. Для збору модуля :app потрібен певний функціонал: Reader, Library тощо. Уявімо ситуацію, що команда локалізації й контенту захотіли мати окремий застосунок, в якому можна давати фідбек і переглядати його в апці. Під ці потреби добре лягає концепція модуляризації всього проєкту. Наприклад, якщо нам потрібен суто Reader (:App-demo-reader), ми беремо всі потрібні фічі, які відносяться до неї, а також модулі, повʼязані з domain- чи з data-логікою, і будуємо залежності.

Демо-апка також може додатково містити модулі, повʼязані з фідбеком до контенту, — певну репортингову систему, яка дозволить слідкувати за якістю контенту, сповіщати про проблеми з ним.


Плюси модульної архітектури:

  1. Краща організація коду. Це дозволяє команді масштабуватися без зайвих проблем.

  2. Легке управління та підтримка. Невеликі функціональні команди зможуть розробляти певні модулі та відповідати за певну область застосунку. Це допоможе пришвидшити розробку.

  3. Зручність тестування. Кожний модуль можна тестувати незалежно, що полегшує виявлення та виправлення проблем. Це призводить до кращого тестового покриття та надійності коду.

  4. Скорочення часу збірки. З правильним підходом до модульної архітектури можна оптимізувати час холодної та гарячої збірки.



Мінуси модульної архітектури:

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

  2. Ризики конфліктів та несумісності в управлінні залежностями.



demo-app-dependency-graph-vs-full-app-dependency-graph

Як розбивати проєкт на модулі: бачення та рекомендації Google


У ґайді з модульної архітектури Google спростовує міф про єдиний варіант розбиття проєкту на модулі за шарами (data, domain, presentation), згідно з ідеєю Clean Architecture. На думку розробників, можна так само розділяти проєкт на модулі суто за сферою використання, тобто певними фічами.


Що ми маємо в такому разі: фіча, яка містить логіку Reader, має також посилатися на прогрес із бібліотеки користувача. Тобто вона буде залежною від інших фіч. У такому випадку ми збільшуємо висоту дерева модулів у проєкті, що призводить до збільшення паралельної збірки. Отже, краще розбивати проєкт і за фічами, і за шарами.


Наприклад, у фічі Reader ми можемо винести в окремий модуль все, що пов'язано з книжками в domain-логіці, в інший модуль — все, що повʼязано з даними. Далі між цими модулями потрібно встановити правильні залежності та отримати гранульовані модулі. В них буде збережена необхідна логіка, яку доволі зручно тестувати та підтримувати. У цьому полягає рекомендація Google, і, забігаючи наперед, це досить вдале рішення.



Принципи розподілення на модулі


Про принципи SOLID знають усі досвідчені розробники — про них питають чи не на кожній технічній співбесіді. Вони допомагають правильно структурувати код, щоби можна було ефективно працювати. Роберт Мартін пропонує принципи звʼязності компонентів, які описують певні аспекти роботи з ними. В цьому контексті компонентом є певна сукупність класів, інтерфейсів. Варто звернути увагу на три принципи:

  1. REP (The Release / Reuse Equivalency Principle) — основна ідея полягає в тому, що компоненти мали окреме версіонування, щоб забезпечити повторне використання коду. Потрібно організувати класи у компоненти, які можна застосувати повторно, а потім відстежувати їх за допомогою релізу. Без номерів версій неможливо забезпечити сумісність усіх повторно використовуваних компонентів один з одним.

  2. CCP (The Common Closure Principle) — цей принцип вказує, які класи слід групувати разом. Багаторазові класи з більшою ймовірністю залежать один від одного, тому вони рідко використовуються окремо. CCP стверджує, що класи компонента повинні бути нероздільними. Це означає, що якщо один компонент залежить від іншого, то він має залежати від усіх його класів, а не від окремих з них. Коротко кажучи, класи, які не мають тісного зв'язку один з одним, не повинні зберігатися в одному компоненті.

  3. CRP (The Common Reuse Principle) — цей принцип вказує, що ми не повинні змушувати користувачів залежати від речей, які вони не збираються використовувати. Тому слід пам'ятати, що модулі в компоненті будуть використовуватися разом. Це означає, що ми маємо переконатися, що класи, які ми розміщуємо в компоненті, є нероздільними, адже неможливо залежати від одних і не залежати від інших. В іншому випадку ми будемо використовувати більше компонентів, ніж це необхідно.


REP-CCP-CRP

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

  • CCP-REP є досить інклюзивними, намагаються зробити модулі більшими, відповідно збільшують зв'язність коду. Але CRP — ексклюзивний принцип, який дотримується тенденції зменшення модулів;

  • CRP-REP — принципи, орієнтовані на повторне використання, прагнуть оптимізовувати модулі для тих команд, які їх використовують. З іншого боку, принцип CCP орієнтований на підтримку, оскільки прагне оптимізовувати модулі для тих, хто їх розробляє.

Отже, ви маєте бути готовими зосередитись на двох підходах, відкинувши третій.


Components-coupling-principles

Згідно з Робертом Мартіном, існує ще низка принципів, які стосуються саме залежностей.


ADP (The Acyclic Dependencies Principle) — про циклічні залежності, а точніше їхню відсутність. Граф залежностей пакетів, модулів не може мати циклів. На схемі вище видно, що фіча С замикається на фічі А. В цьому випадку є два рішення:

  1. Використовувати окремий модуль із залежністю, від якого залежить С і А. Це рішення підходить, якщо використання цього допоміжного модуля не буде поодиноким випадком.

  2. Додати в модуль С певний інтерфейс, який дозволить комунікувати з фічею А через модуль :app. Рішення підходить для поодинокого випадку.

SDP (The Stable Dependencies Principle) — принцип стабільних залежностей. Наприклад, ми маємо розповсюджені модулі для роботи з базами даних. Вони є стабільними, тому на них базується більша частина системи. Якщо певні модулі мають велику кількість використань, вони вважаються більш стабільними. Вся ієрархія залежностей має будуватися від найбільш стабільного до найменш. Тобто модуль :app завжди буде найменш стабільним, адже він змінюється від задачі до задачі, інкапсулюючи в собі частину логіки, яка потрібна для зведення певного функціонала навігації.


SAP (The Stable Abstractions Principle) — абстракція підвищує стабільність. Йдеться про те, що ми завжди хочемо залежати від абстракції та змінювати тільки реалізацію певних компонентів у системі.


dependency-graph-in-gradle


Час збірки у модульній архітектурі


Розглянемо наш граф залежностей в Gradle. Використання модулем :app фічі Reader — це ребро графу. Час збірки доволі сильно залежить від висоти нашого дерева. В холодній збірці ми спочатку збираємо найбільш стабільні модулі та розпаралелюємо побудову дерева за залежними модулями. Тобто спершу у нас будуватиметься модуль з ремоут-конфігами. Далі перебудовується модуль аналітики та паралельно створюється модуль з даними. Після цього будуватиметься модуль з фічею.


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



Як ми починали роботу над модуляризацією


Раніше наш проєкт можна було назвати монолітом. У ньому була виокремлена та розбита за шарами частина з data- та domain-логікою, а також стилі, графіка та дизайн системи. Через те, що ми не застосовували підхід із розділенням за фічами, така архітектура призводила до певних складнощів при збільшенні команди.

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


Ми не користувалися стандартними рішеннями, запропонованими Google, а використовували власне рішення на базі підходу координаторів, адже потребували доволі гнучкої логіки у відкритті певних екранів і певних флоу, залежно від різних станів застосунку. Це дозволило сегрегувати запуск тестів на окремі модулі.


Ми використовуємо підхід до модуляризації варіантів екранів або фічей. Чому це важливо в нашому контексті? Наприклад, в корені in-app модуля і payment-модуля можна викласти API для роботи з платежами, і на базі нього посилатися на певні фічі всередині in-app payment. Саме цей підхід вирішив проблему із залежностями під час A/B-тестування, а також дозволив більш ефективно зводити функціонал і зберігати ресурси окремо. Достатньо видалити умовно модуль з певним функціоналом і на цьому наша робота з прибиранням зайвого коду закінчена. Таким чином, ми зводимо до мінімуму ризики неправильної підчистки певних фічей, і це призводить до більшої стабільності.


Наразі проєкт має близько 50-60 модулів, і процес модуляризації продовжується — найближчим часом плануємо розділяти data- та domain-логіку.


Обираючи підхід для свого проєкту, враховуйте, що універсального рішення, яке підходить усім, не існує. Будуйте архітектуру так, щоби вона в першу чергу органічно підходила вашій команді.



Корисні матеріали

Підписуйся на нашу розсилку та отримуй корисні матеріали першим!

Надаючи вашу електронну адресу, ви погоджуєтесь з нашою Політикою приватності.

Дякуємо, що підписалися.

image-from-rawpixel-id-5996033-png.png
bottom of page