Модульність — це принцип, за якого застосунок збирається з незалежних модулів, кожен із яких відповідає за одну функціональність. За даними Android Developers, поділ на модулі прискорює збірку завдяки паралельній компіляції та дозволяє командам працювати над різними частинами застосунку незалежно. Модульна архітектура стала стандартом для великих мобільних проєктів із десятками розробників.
Головне
Модульність — це спосіб організації коду, за якого застосунок складається зі слабко зв'язаних модулів, кожен із яких надає строго визначену функціональність через публічний інтерфейс. На відміну від монолітної архітектури, де всі класи знаходяться в одному проєкті, модульний підхід розділяє код на фізично незалежні одиниці збірки.
Основна мета модульності — управління складністю. Розробник може зосередитися на одному модулі, не тримаючи в голові всю кодову базу. Кожен модуль має свою зону відповідальності та може розроблятися, тестуватися і розгортатися незалежно від інших. Це особливо цінно в проєктах із 10+ розробниками, де паралельна робота над монолітом призводить до частих конфліктів злиття.
Важливо відрізняти модульність від шаруватої архітектури. Шари (Presentation, Domain, Data) ділять код за технічною ознакою, а модулі — за функціональною. Модуль «Профіль користувача» може містити власні шари всередині. На практиці модульний підхід і шарувата архітектура комбінуються: кожен модуль має власну тришарову структуру.
Feature-модулі — найпопулярніший вид модулів. Кожен екран або група пов'язаних екранів виділяється в окремий модуль: Onboarding, Profile, Settings, Feed. Feature-модуль містить все необхідне для роботи функції: UI, бізнес-логіку, data-шар. Межі модуля захищені — інші функції не можуть отримати доступ до його внутрішніх класів.
Core-модулі містять загальну інфраструктуру: роботу з мережею, базою даних, аналітикою, дизайн-системою. Вони не залежать від feature-модулів, але feature-модулі залежать від них. Такий поділ гарантує, що зміна аналітичного SDK не вплине на мережевий шар, і навпаки. Core-модулі повторно використовуються між фічами без дублювання коду.
Shared-модулі містять код, який використовується кількома фічами: моделі даних, утиліти, константи, кастомні View. Основна проблема shared-модулів — ризик перетворення на звалище («misc module»), де з часом накопичується різнорідний код. Правило: shared-модуль повинен мати чітку тематику, наприклад «shared-ui» або «shared-models».
В Android shared-модулі часто виділяють у бібліотеки з префіксом lib: lib-network, lib-database, lib-ui-components. В iOS ті ж функції виконують внутрішні Swift Packages всередині Workspace. На практиці команди обмежують кількість shared-модулів 3–5, щоб не створювати надмірну мережу залежностей, що ускладнює збірку.
Окремі test-модулі дозволяють запускати тести лише для зміненого модуля, не прогоняючи всю тестову базу. Це скорочує час CI/CD пайплайну з годин до хвилин. Модульна ізоляція забезпечує SoC на рівні збірки: модуль мережевого шару не може випадково імпортувати UI-бібліотеки у своїх тестах.
Кожен модуль повинен мати чітко визначений публічний API. В Android це досягається через access modifiers та api vs implementation в Gradle. В iOS — через public/internal модифікатори доступу та керовані залежності через Package.swift. Скорочення видимості до мінімально необхідної — ключова практика модульного дизайну.
Gradle підтримує модульну архітектуру нативно: кожен модуль — це окрема одиниця збірки зі своїм build.gradle файлом. Android проєкти використовують комбінацію application модуля (app) та кількох library модулів. Бібліотечні модулі не можуть бути запущені як застосунок, але можуть публікуватися як AAR в репозиторій.
Ключова фіча Gradle — паралельна збірка незалежних модулів. Якщо modules A, B та C не залежать один від одного, Gradle компілює їх одночасно, використовуючи всі ядра процесора. У проєктах із 20+ модулями це скорочує повну збірку з 15 до 3–5 хвилин. Інкрементальна збірка зміненого модуля займає секунди.
Gradle надає два типи залежностей між модулями: api (транзитивні) та implementation (нетранзитивні). Різниця критично важлива для модульності: implementation приховує транзитивні залежності від споживачів модуля. Якщо модуль :profile використовує :networking через implementation, споживачі :profile не знають про :networking і не можуть до нього звернутися.
// settings.gradle — декларація модулів
include ':app'
include ':feature:profile'
include ':feature:settings'
include ':core:network'
include ':core:database'
// build.gradle feature/profile — залежності модуля
dependencies {
implementation project(':core:network')
implementation project(':core:database')
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0'
}
Код показує структуру модульного Android-проєкту. Settings.gradle перелічує всі модулі, а build.gradle кожного feature-модуля вказує лише ті core-модулі, які йому потрібні. Build system автоматично розв'язує транзитивні залежності та збирає модулі в правильному порядку.
Swift Package Manager (SPM) — стандартний інструмент модульності в iOS з 2019 року. SPM дозволяє розбивати застосунок на Swift Packages, кожен із яких може бути library або executable. Package визначає модулі (targets) та їх залежності через Package.swift. SPM інтегрований в Xcode і не потребує додаткових інструментів.
CocoaPods залишається основним менеджером залежностей для сторонніх бібліотек. Podfile та Podspec задають модульну структуру, а CocoaPods генерує workspace з окремими pod-проєктами. Для власної модульності проєкту команди все частіше обирають SPM, оскільки він вбудований в Xcode і не потребує встановлення.
В iOS модульності важливу роль відіграє access control: public, package, internal, fileprivate та private. Модуль публікує лише ті типи, які мають бути доступні іншим модулям. Внутрішні деталі реалізації приховані за internal та private модифікаторами. Це запобігає появі прихованих залежностей між модулями.
// Package.swift — модульна структура iOS проєкту
let package = Package(
name: "MyApp",
platforms: [.iOS(SupportedPlatform.iOSVersion.v17)],
products: [
.library(name: "ProfileFeature", targets: ["ProfileFeature"]),
.library(name: "NetworkCore", targets: ["NetworkCore"]),
],
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.9.0")
],
targets: [
.target(name: "ProfileFeature", dependencies: ["NetworkCore"]),
.target(name: "NetworkCore", dependencies: ["Alamofire"]),
]
)
Package.swift оголошує два library-продукти: ProfileFeature та NetworkCore. ProfileFeature залежить від NetworkCore, але не знає про існування Alamofire — той прихований всередині NetworkCore. Така ізоляція — пряме застосування SoC на рівні модулів: зміни в HTTP-клієнті не потребують перекомпіляції ProfileFeature.
Основна перевага модульності — швидкість розробки. Команди працюють паралельно над різними модулями без конфліктів у коді. CI/CD пайплайн збирає лише змінені модулі та запускає лише їх тести. Час фідбеку скорочується, а частота релізів зростає. Spotify, Uber та Airbnb публікували кейси міграції на модульну архітектуру з покращенням метрик у 2–3 рази.
Друга перевага — ізоляція помилок. Баг у модулі Profile не впливає на модуль Payments, якщо між ними немає прямих залежностей. Це особливо важливо в застосунках із високоризиковою функцією (платежі, медичні дані), де помилка в непов'язаному екрані не повинна блокувати реліз критичного функціоналу.
Головна складність — управління залежностями. При неправильному проєктуванні виникає граф модулів, де зміна одного модуля каскадно перезбирає десятки інших. Рішення — слідувати правилу ациклічності: граф залежностей модулів має бути спрямованим ациклічним графом (DAG). Інструменти на кшталт Gradle Module Graph Assert допомагають виявити цикли на етапі збірки.
Друга складність — зростання часу початкового налаштування. Створення модульної архітектури потребує більше часу на етапі ініціалізації проєкту. Невеликі проєкти з 1–3 розробниками можуть не отримати вигоди від модульності, витрачаючи час на підтримку меж модулів без реальної потреби в розпаралелюванні. Рішення — починати з моноліту та виділяти модулі в міру зростання команди.
Feature-first підхід групує модулі за функціональністю: кожен екран або група екранів стає окремим модулем. Layer-first підхід ділить код за технічною ознакою: окремі модулі для UI, бізнес-логіки та даних. На практиці більшість команд обирають feature-first з core-модулями — це дає кращу ізоляцію та зрозумілу навігацію по проєкту.
Вибір між підходами залежить від розміру команди та передбачуваності функціональності. Якщо ви точно знаєте, які екрани будуть у проєкті, feature-first дозволяє кожному розробнику відповідати за свій модуль. Якщо функціональність часто змінюється та перетинається між екранами, layer-first дає більше гнучкості при повторному використанні коду між різними фічами.
Часто задавані питання
Оптимальна кількість залежить від розміру проєкту та команди. Для команди з 5 осіб достатньо 6–10 модулів. Для 20+ розробників — 20–40 модулів. Правило: модуль має бути достатньо малим, щоб один розробник розумів його цілком, і достатньо великим, щоб не створювати надмірну мережу залежностей.
Правильна модульність прискорює збірку завдяки паралельній компіляції та кешуванню. Але надмірна кількість модулів із щільними залежностями сповільнює збірку — Gradle та Xcode витрачають час на розв'язання графа. Ключ до швидкої збірки — мінімізація транзитивних залежностей та дотримання ациклічності.
Так, але ітеративно. Почніть із виділення core-модулів (мережа, база даних), потім виносьте фічі по одній. Використовуйте feature flags, щоб вмикати новий модульний код паралельно зі старим монолітним. Повна міграція великого застосунку займає від 3 до 12 місяців.
Модулі — це одиниці компіляції всередині одного застосунку. Мікросервіси — окремі процеси, що працюють на різних серверах. Модулі розділяють код, мікросервіси розділяють runtime. У мобільній розробці часто використовується термін «microapps» як гібрид: feature-модулі, які можуть запускатися як самостійні застосунки.
Кожен модуль має власні Unit-тести, що запускаються незалежно. Integration-тести перевіряють взаємодію між модулями. UI-тести покривають feature-модулі з mock-даними. Модульна архітектура спрощує тестування: замокати залежність іншого модуля простіше, ніж замокати частину моноліту.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також