Цель этой статьи — поделиться с вами опытом, приобретенным в результате масштабирования приложения с нуля до более чем 150 модулей. Мы расскажем вам о трудностях и подводных камнях, с которыми мы столкнулись на этом пути, а также продемонстрируем эволюцию нашего приложения и команды за последние пять лет.

Трудности, о которых здесь пойдет речь, связаны со сборкой продукта, который стремится иметь только один (моно-)репозиторий для всего функционала проекта.

Наблюдения, представленные ниже, описывают то, что происходило во время создания приложения для банкинга Stone.

На изображении выше показано, какой была первоначальная организация команды, т.е. когда вся команда одновременно отвечала за вес функционал.

Модуляризация выходит за рамки Gradle

Одной из основных целей модуляризации приложения является сокращение времени сборки. Поэтому, когда мы организуем структуру нашего проекта в виде модулей, они должны иметь как можно меньше зависимостей. Благодаря этому Gradle будет выполнять их параллельно, тем самым оптимизируя время сборки.

Модуляризация — одна из многих вещей, которые можно сделать, чтобы сократить время сборки. Добавление некоторых базовых и продвинутых конфигураций Gradle также может помочь сократить время сборки. Если вы хотите узнать больше о конфигурациях Gradle, то очень рекомендую вам контен Тони Робалика (Tony Robalik) и Адама (Adam). Отличными отправными точками будут эта и эта статьи.

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

«Учитывая сказанное выше, почему мы все-таки решили пойти дальше оптимизации настроек Gradle?»

Каждый проект и каждая компания имеют свою уникальную структуру, которая в свою очередь может формировать самую разную структуру кода. Ваш код и его структура, как правило, являются отражением организации вашей компании. Это явление известно под названием «закон Конвея».

Если в двух словах, то закон Конвея гласит, что структура организации отражается на процессе создания кода. Это не хорошо и не плохо, но это достаточно интересно.

В дополнение к Gradle очень полезно понимать, как устроены структура организации и взаимодействие с командой Android, и как выглядит ежедневный процесс разработки продукта. Кроме того, понимание структуры может значительно облегчить процесс модуляризации.

Подходить к тому, как и какие модули вы хотите создать, нужно очень аккуратно. Золотого правила, как это сделать, не существует. На следующие идеи влияют структура и размер команды по мере развития проекта.

Модульность в самом начале 

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

В начале архитектура нашего приложения предполагала использование трех типов модулей:

  • Базовые (Base): модули, которые создают ценность для разработчиков, например, вся логика для выполнения запроса, служебные функции, функции-расширения и т. д. Эти модули представлены фиолетовым цветом. Например, модуль utils существовал для хранения всех ресурсов приложения (изображений, строк, расширений) в одном месте.

  • Фичи (Feature): модули, создающий прямую ценность для клиента, то есть функционал приложения, представленные желтым цветом.

  • Бизнес (Business): модули, который содержит все бизнес-правила всего функционала, представленные на изображении синим цветом.

Чтобы создать новую фичу в соответствии со структурой выше, нам нужно было добавить новый модуль, начинающийся со слова "feature", затем нам нужно было добавить соответствующую часть в бизнеса правила (модуль Domain). И, наконец, модифицировать служебный модуль, отвечающий за выполнение запросов к API.

Эта структура была оправдана для всей команды. В приложении было чуть больше десятка модулей и очень мало конфликтов в кода, не смотря на то, что вся команда постоянно меняла общие части.

Философия, стоявшая за модульной структурой, показанной на рисунке выше, была довольно проста: создать как можно больше модулей Kotlin, поэтому мы пришли к многослойной модуляризации. Этот вид структурирования разделяет код одного и того же функционала между разными модулями. Так, например, в Feature-модуле вы найдете часть кода, относящуюся к экрану, который увидит пользователь; в модуле Domain  — бизнес-правило или принцип работы этого функционала; а в сервисном модуле весь код, связанный с запросом и ответом от сервера/API.

Эта структура наглядно отражает рабочие будни нашей команды, поскольку ни один из членов не нес исключительную ответственность за какой-либо конкретный функционал.

Прошло какое-то время, и теперь у нас 200 000 строк кода и команда побольше

Двести тысяч строк кода не кажутся таким уж впечатляющим числом, но следует отметить, что в проекте не использовались ни библиотеки для генерации кода, такие как Dagger, ни процессоры аннотаций Kotlin (kapt).

Модульная структура из предыдущего раздела оставалась неизменной около трех лет. Тем не менее, в дополнение к основному проекту у команды теперь было пять репозиториев, три разные библиотеки и один побочный проект для поддержки основного проекта. Таким образом, в дополнение к 200 000 строк кода в основном проекте нужно было поддерживать еще четыре кодовых базы меньшего размера.

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

Теперь команда владела несколькими продуктами в разных репозиториях
Теперь команда владела несколькими продуктами в разных репозиториях

Эта структура с несколькими репозиториями создала некоторые проблемы:

  • Поддержка версии SDK. Потеря времени CI/CD (непрерывная интеграция/непрерывное развертывание) на создание новых версий, необходимых для основных и дополнительных проектов.

  • Обновляя конфигурации, необходимо было создавать множество пул-реквестов, чтобы поддерживать актуальные версии Kotlin, Gradle и библиотек с зависимостями в проектах.

  • Дублирование кода и некоторые стандартные определения в проектах и ​​библиотеках, выливались в то, что ошибка, исправленная в одном репозитории, не обязательно была исправленной во всех остальных.

Эта структура сильно повлияла на производительность команды. Было создано немало нелепых пул-реквестов, чтобы навести порядок.

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

  • Для разных Gradle-структур скрипты отличались, что-то было на Kotlin, что-то на Groovy.

  • Генерация артефактов. Как сгенерировать набор разных SDK и проектов из одного репозитория?

  • Конфликты между похожими абстракциями, но с разными реализациями.

  • Различные версии Kotlin для разных проектов.

  • Циклическая зависимость при миграции модулей.

Миграция на монорепозиторий оказалась отличным решением. Это значительно снизило стоимость конфигурации и обновления зависимостей. Кроме того, это положительно сказалось на производительности, поскольку больше не нужно было приостанавливать разработку фичи из-за ожидания новых артефактов из некоторых поддерживаемых библиотек/SDK.

Но эта миграция не отменяет необходимость разработки нового функционала.

По возможности избегайте универсального модуля utils

После перехода на монорепозиторий модуль utils проявил себя, как не очень хорошая идея, потому что в какой-то момент это он стал местом практически для всех новых абстракций, определяемых командой. Там можно было найти все виды кода, даже если между ними не было вообще никакой связи. Со временем почти все остальные модули нашего функционала зависели от utils.

Гораздо лучше создать набор модулей с четко определенными абстракциями, используемыми отдельно в разных частях приложения. Так, например, вместо того, чтобы иметь utils/threads, где потоки являются частью этого служебного модуля, гораздо лучше создать модуль util-threads, сосредоточив внимание такого модуля на решении конкретной задачи или предоставляя лишь небольшую часть абстракции, используемой приложением.

Вывод здесь заключается в том, что если вы собираетесь создать модуль без четко определенного назначения, то лучше не делайте этого, пройдет время, и удаление или реорганизация этого модуля в будущем может дорого обойтись.

Время сборки начало расти. Проблема решилась (деньгами)

Прошло три года, а компьютеры у команды были все те же. Однако, несмотря на модульность нашего проекта, время сборки перестало быть приемлемым, а оборудование, которое было в нашем распоряжении, могло быть и получше. Поэтому было принято самое простое и мудрое решение — обновить инструментарий, доступный команде, что решило большую часть проблем.

Итак, обновляйте компьютеры команды, когда это возможно. Требуйте этого от своего начальства - производительность команды значительно возрастет, а вложенные средства окупятся с торицей. Если не верите, то можете почитать другие статьи, в которых об этом говорится.

Исследование времени сборки

В рамках исследования времени сборки этот доклад очень помог мне понять, как может развиваться модульность проекта.

Тиаго (Thiago) создал этот пример проекта, чтобы проверить на практике популярные идеи. Он состоит из достаточно большого количества модулей. Он был задуман таким образом, чтобы разобрать и понять реализацию разных идей различных модульных структур.

Вот некоторые выводы из этого исследования:

  • Отказ от buildSrc может помочь. Используйте included build.

  • Определите отношения между модулями и сосредоточьтесь на горизонтальной иерархии модулей. Например, Feature-модуль не может зависеть от другого Feature-модуля, что позитивно сказывается на глубине вашего графа сборки.

  • Объедините модули, использующие одно и то же бизнес-правило.

  • Внимательно следите за обновлениями/новыми фичами Gradle.


Всех начинающих Android-разработчиков приглашаем на открытое занятие «Управление базой данных в андроиде на примере Room». На этом уроке научимся подключать рум к проекту, попробуем сделать простейшие запросы в базу данных, транзакции на запись и на чтение. Научимся пользоваться базовой функциональностью рума. Регистрация открыта по ссылке.

Комментарии (2)


  1. DinoZavr2
    25.11.2022 22:44

    Хочу отметить, изображения отличаются от оригинальных. Вы не так обрезали, либо скачали через Яндекс.


    1. MaxRokatansky Автор
      26.11.2022 00:45

      Спасибо, что заметили. Поправил