Модулността е принцип, при който приложението се сглобява от независими модули, всеки от които отговаря за една функционалност. Според Android Developers, разделянето на модули ускорява компилацията чрез паралелно компилиране и позволява на екипите да работят по различни части на приложението независимо. Модулната архитектура се превърна в стандарт за големи мобилни проекти с десетки разработчици.
Основни точки
Модулността е начин за организиране на код, при който приложението се състои от слабо свързани модули, всеки от които предоставя строго определена функционалност чрез публичен интерфейс. За разлика от монолитната архитектура, където всички класове са в един проект, модулният подход разделя кода на физически независими единици за компилиране.
Основната цел на модулността е управлението на сложността. Разработчикът може да се фокусира върху един модул, без да държи цялата кодова база в главата си. Всеки модул има своя зона на отговорност и може да се разработва, тества и внедрява независимо от останалите. Това е особено ценно в проекти с 10+ разработчици, където паралелната работа върху монолит води до чести конфликти при сливане.
Важно е да различаваме модулността от слоистата архитектура. Слоевете (Presentation, Domain, Data) разделят кода по технически критерий, а модулите — по функционален. Модулът \u201eПотребителски профил\u201c може да съдържа собствени слоеве вътре в себе си. На практика модулният подход и слоистата архитектура се комбинират: всеки модул има своя трислойна структура.
Feature модули — най-популярният вид модули. Всеки екран или група свързани екрани се отделя в самостоятелен модул: Onboarding, Profile, Settings, Feed. Feature модулът съдържа всичко необходимо за работата на функцията: UI, бизнес логика, слой за данни. Границите на модула са защитени — други функции нямат достъп до неговите вътрешни класове.
Core модули съдържат общата инфраструктура: работа с мрежа, база данни, аналитика, дизайн система. Те не зависят от feature модулите, но feature модулите зависят от тях. Такова разделение гарантира, че промяната на аналитичния SDK няма да засегне мрежовия слой и обратно. Core модулите се използват повторно между функциите без дублиране на код.
Shared модули съдържат код, използван от няколко функции: модели на данни, помощни средства, константи, персонализирани изгледи. Основният проблем на shared модулите е рискът да се превърнат в бунище (\u201emisc module\u201c), където с времето се натрупва разнороден код. Правило: shared модулът трябва да има ясна тема, например \u201eshared-ui\u201d или \u201eshared-models\u201c.
В Android shared модулите често се отделят в библиотеки с префикс lib: lib-network, lib-database, lib-ui-components. В iOS същите функции изпълняват вътрешни Swift Packages в рамките на Workspace. На практика екипите ограничават броя на shared модулите до 3–5, за да не създават прекомерна мрежа от зависимости, която усложнява компилацията.
Отделни тестови модули позволяват пускането на тестове само за променения модул, без да се стартира цялата тестова база. Това съкращава времето на CI/CD pipeline от часове на минути. Модулите осигуряват разделяне на ниво компилация: модулът на мрежовия слой не може случайно да импортира UI библиотеки в тестовете.
Всеки модул трябва да има ясно дефинирано публично API. В Android това се постига чрез модификатори за достъп и api vs implementation в Gradle. В iOS — чрез public/internal модификатори за достъп и управлявани зависимости чрез Package.swift. Намаляването на видимостта до минимум необходимо е ключова практика на модулния дизайн.
Gradle поддържа модулна архитектура нативно: всеки модул е отделна единица за компилиране със собствен build.gradle файл. Android проектите използват комбинация от application модул (app) и няколко library модула. Библиотечните модули не могат да се стартират като приложение, но могат да се публикуват като AAR в хранилището.
Ключовата функция на Gradle е паралелното компилиране на независими модули. Ако модулите 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 модули, от които се нуждае. Системата за компилиране автоматично разрешава транзитивните зависимости и компилира модулите в правилния ред.
Swift Package Manager (SPM) — стандартният инструмент за модулност в iOS от 2019 г. SPM позволява разделяне на приложението на Swift Packages, всеки от които може да бъде библиотека или изпълним файл. Package дефинира модулите (targets) и техните зависимости чрез Package.swift. SPM е интегриран в Xcode и не изисква допълнителни инструменти.
CocoaPods остава основният мениджър на зависимости за библиотеки на трети страни. Podfile и Podspec дефинират модулната структура, а CocoaPods генерира workspace с отделни pod проекти. За собствената модулност на проекта екипите все по-често избират SPM, тъй като е вграден в Xcode и не изисква инсталация.
В iOS модулността контролът на достъпа играе важна роля: 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 декларира два библиотечни продукта: ProfileFeature и NetworkCore. ProfileFeature зависи от NetworkCore, но не знае за съществуването на Alamofire — той е скрит вътре в NetworkCore. Такава изолация е пряко приложение на разделянето на модулно ниво: промените в HTTP клиента не изискват прекомпилиране на ProfileFeature.
Основното предимство на модулността е скоростта на разработка. Екипите работят паралелно по различни модули без конфликти в кода. CI/CD pipeline компилира само променените модули и пуска само техните тестове. Времето за обратна връзка се намалява, а честотата на пускане се увеличава. 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 месеца.
Модулите са единици за компилиране в рамките на едно приложение. Микросървисите са отделни процеси, работещи на различни сървъри. Модулите разделят кода, микросървисите разделят средата за изпълнение. В мобилната разработка често се използва терминът \u201emicroapps\u201d като хибрид: feature модули, които могат да се стартират като самостоятелни приложения.
Всеки модул има свои единични тестове, стартирани независимо. Интеграционните тестове проверяват взаимодействието между модулите. UI тестовете покриват feature модули с mock данни. Модулната архитектура опростява тестването: mock-ването на зависимост от друг модул е по-лесно от mock-ването на част от монолит.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също