Когда в приложении больше десятка разработчиков и сотни тысяч строк, без модулей сборка занимает минуты, а изменения в одной фиче ломают другую. На middle и senior собесах спрашивают, как делить приложение и как не превратить модули в новый клубок.
Зачем
- Скорость сборки: изменился модуль, пересобираются он и его потребители, а не всё.
- Границы: модуль открывает публичный API и прячет реализацию.
- Параллельная работа: команды владеют своими модулями.
- Переиспользование: модуль можно подключить в другое приложение или расширение.
Размер приложения и скорость запуска модульность сама по себе не улучшает. На iOS десятки динамических фреймворков даже замедляют холодный старт.
Типы модулей
:app сборка и склейка зависимостей
:feature:cart:api интерфейсы и точки входа фичи
:feature:cart:impl реализация: экраны, view model, data
:core:network сетевой клиент
:core:database хранилище
:core:designsystem UI-компоненты
:core:analytics
:testing общие фейки и утилиты тестов
Правила зависимостей
- Фича зависит от core и от api других фич, но никогда от их impl.
- Циклов нет (Gradle и SPM их и не допустят).
- Склеивает реализации app-модуль через DI.
Переход из корзины на карточку товара:
// :feature:product:api
interface ProductNavigator { fun openProduct(id: String) }
// :feature:cart:impl зависит только от product:api
class CartViewModel(private val productNavigator: ProductNavigator)
Если два модуля зависят друг от друга, граница проведена неверно: общую часть выносят в третий модуль или вводят интерфейс в нижнем модуле.
Почему сборка может не ускориться
Если все модули зависят от огромного core, который правят каждый день, любое изменение пересобирает всех. Помогают:
- маленькие стабильные core-модули;
- разделение api/impl;
- в Gradle
implementationвместоapiтам, где зависимость не торчит в публичных сигнатурах: тогда её изменения не пересобирают потребителей; - контроль графа на CI.
Платформенные детали
iOS: локальные Swift-пакеты (SPM) или фреймворки в Xcode-проекте, генераторы Tuist или XcodeGen, в больших компаниях Bazel. Модули линкуют статически, динамическими оставляют только код, общий для приложения и расширений.
Android: Gradle-модули, convention plugins для общей конфигурации, version catalog для версий. Домен удобно держать в чистых Kotlin-модулях, они собираются быстрее Android-библиотек. Dynamic Feature Modules позволяют загружать части приложения по требованию.
Flutter: пакеты в монорепозитории (melos). React Native: пакеты в монорепозитории (workspaces).
Демо-приложения
Для крупной фичи делают маленькое приложение, которое собирает только её с фейковыми зависимостями. Итерации быстрее, а заодно проверяется независимость: если фичу нельзя запустить отдельно, связи слишком сильные.
Модуляризация монолита
- Измерить время сборки и граф зависимостей.
- Вынести инфраструктуру в core.
- Новые фичи писать сразу модулями.
- Старые выносить по одной, начиная с самых часто меняющихся: найти границы, разорвать зависимости, описать API, перенести код, подключить через DI.
- Проверять правила графа на CI, владельцев модулей закрепить в CODEOWNERS.
Что спросят на собесе
- Зачем делить приложение на модули и какие бывают модули?
- Как организовать переход между фичами в разных модулях?
- Почему после модуляризации сборка может не ускориться?
- Статические или динамические модули на iOS?
- Как модуляризировать монолит без остановки разработки?