Наши дальнейшие действия
В итоге мы провели ревизию нашей многослойной структуры, так как из-за того, что был изменен целый ряд модулей, мы больше не могли использовать кэш Gradle - нам все-равно нужно было пересобирать его. Это конечно же отразилось на времени, необходимом для запуска приложения.
Затем мы немного упростили код нашего функционала, чтобы бизнес-правило было к нему ближе и в то же время изолированно от других модулей. В результате структура функциональных модулей стала выглядеть следующим образом:
Сервис, домен и представление стали ближе и больше связаны.
Таким образом, изменение или создание фичи затрагивало меньше модулей, что позволило намного лучше использовать кэш Gradle.
Одним из преимуществ этого подхода было то, что код фичи стал намного более осязаем и, как следствие, проще для понимания, поскольку он был меньше раскидан по модулям. И к этому моменту это уже перешло в разряд необходимости, поскольку держать в голове код всего проекта было уже просто невозможно.
Организация нашей структуры в итоге преобразовалась из простой многослойной в структуру, содержащую модули с функционалом и вспомогательные модули. Networking (сетевое взаимодействие) является хорошим примером вспомогательного модуля, внутри которого определена вся retrofit-конфигурация. Этот модуль затем используется модулем с функционалом, которому нужно только создать retrofit-интерфейс с конечными точками, необходимыми для фичи.
В рамках этого разделением мы пересмотрели и наш процесс создания фичи. Мы заметили, что у нас было много общего кода, и чем более изолированными будут подобные модули, тем меньше будет таких проблем, как, например, конфликты в коде и изменения, косвенно нарушающие функциональность наших фич.
Реструктуризация команды и во что превратился наш проект
На данный момент наш проект превратился в супер-приложение, цель которого заключается в том, чтобы предоставить пользователю доступ к широкому набору функциональных возможностей, доступных в продуктах нашей компании.
Чтобы создать супер-приложение, нашей команде нужна была более обширная кодовая база, чем та, что была в нашем изначальном приложении. Но даже после объединения двух продуктов, релизы всех наших продуктов должны были продолжать идти по графику. Учитывая тот объем функционала, который уже существовал, мы нуждались в пересмотре структуры нашей команды. Раньше у нас была стандартная иерархическая модель. На смену ей пришли матричные структуры команд по типу племен (tribes - проектных команд), отрядов (squads - базовая единица) и т.д.
Независимые команды создают разный, не связанный между собой, функционал.
Переход на эту распределенную модель позволил улучшить организацию нашего проекта. Например, помимо команд, отвечающих за клиентский функционал, была создана команда, отвечающая за платформу Android, в обязанности которой входила поддержка команд, разрабатывающих фичи.
В рамках этой реорганизации, мы смогли разделить ответственность за различные модули приложения между несколькими функциональными и платформенными командами. В итоге мы пришли к следующему разделению:
Устаревшие (Legacy) модули: все модули с этой номенклатурой должны быть постепенно удалены из нашего проекта в угоду более изолированной структуры проекта. Например, модуль domain, выделенный синим цветом на изображении исходной структуры, должен быть удален, поскольку бизнес-правила теперь являются частью функциональных модулей.
У нас было несколько таких устаревших модулей, и каждая команда отвечала за перенос необходимой части этого кода в свои модули в рамках новой структуры.
Модули фич (Feature): модули, которые создают прямую ценность для клиента. В ответственности каждой функциональной команды теперь есть определенный набор таких модулей, за которые они отвечают.
Платформенные (Platform) модули: модули, которые предоставляют командам, занимающимся функционалом, ресурсы. Сюда входит, например, вся сетевая логика, безопасность, кэширование, аналитика, внедрение зависимостей, фича-флаги, системы для проектирования, производительность и т.д.
Вспомогательные (Support) модули: модули, в рамках которых все еще осталось какое-либо пересечение между несколькими командами, например, домашняя страница приложения, где можно осуществить переход к разным продуктам.
Каждая команда отвечает за свой продукт в рамках супер-приложения.
Сокращение времени сборки с помощью Focus
Благодаря более четкой связи между различными модулями у нас появилась возможность добавить Focus, разработанный командой Dropbox. Плагин Focus помогает сфокусировать ресурсы на сборке конкретного модуля, избавляя от необходимости синхронизировать при этом весь моно-репозиторий. Кроме того, Габриэль Соуза (Gabriel Souza) создал плагин для Android Studio поверх Focus, чтобы еще больше упростить с ним работу. С его помощью вам нужно лишь кликнуть по модулю, выбрать “Focus” и дождаться, пока Gradle обновит проект.
Добавление в проект sample app’ов
Все проекты приложений имеют главный app-модуль с примененным плагином приложения; при создании нового модуля в том же проекте Android Studio добавит плагин нашей библиотеки.
App-модуль должен иметь зависимости от всех остальных модулей нашего приложения, чтобы при создании apk был доступен весь функционал.
Sample app помогает нам ограничится созданием только определенной части приложения, т. е. вместо того, чтобы иметь зависимости от всех остальных модулей, sample app будет полагаться на меньший набор модулей, что может значительно сократить время сборки.
Поскольку каждый отряд теперь отвечает только за определенную группу модулей, в рамках разработки проекта были созданы разные sample app, чтобы им необходимо было создавать только часть приложения.
С правой стороны модули, доступные после применения плагина Focus
Вот несколько важных моментов, учитываемых при создании sample app:
Оно должно проходить процесс аутентификации так же, как и полноценный app-модуль, чтобы команда работала в рамках такой же пользовательской сессии, которая будет в продакшене. То есть оно должно иметь зависимость от модуля логина.
Добавляйте в него только те модули, над которыми вы работаете.
Обязательно создайте активити/представление/композицию/фрагмент, которое будет следующим пунктов в навигации после аутентификации.
Соответствующим образом перепишите навигацию для нового экрана, созданного на предыдущем шаге.
Тут есть две основные задачи; во-первых, навигация после завершения процесса аутентификации и сборка графа библиотеки, используемого для внедрения зависимостей. В нашем проекте используется Kodein.
Автоматизация создания sample app способствует лучшему принятию его командой. Скрипт выполняет следующие действия:
Автоматически создает sample app.
Для решения задачи внедрения зависимостей был создан новый класс SampleAppApplication. Этот класс помогает структурировать внедрение зависимостей для sample app.
Сгенерированное активити представляет собой список кнопок, при нажатии которых осуществляется переход к соответствующему функционалу в Focus.
После запуска скрипта человек, добавляющий sample app, должен обновить существующий список навигации в классе SampleAppApplication, который ожидает возврата типа map<String, Screen>. Реализация выглядит примерно так:
fun listOfEntryPoints() = mapOf(
"Button Text" to Navigation.Home,
"Second flow to navigate" to Navigation.CopyAndPaste
)
Навигация в этом проекте работает через Screen. Это закрытый класс, в котором есть маппинг на какой-нибудь экран.
Помимо навигации, необходимо было определить, какие зависимости от других модулей должны быть у нашего sample app. Для этого нам просто нужно было обновить файл build.gradle.kts
:
dependencies {
implementation (project(":module-xyz)")
...
}
Этот скрипт тесно связан со структурой проекта, так как он связан с системой навигации приложения и внедрением зависимостей.
Отражение структуры команды в проекте
Проект, содержащий такое большое количество модулей, в итоге был организован таким образом, чтобы в нем можно было легко ориентироваться.
В настоящее время структура проекта отражает организацию наших команд. У нас есть несколько отрядов в рамках разных племен, добавляющих фичи в проект, поэтому для участника команды было бы вполне естественно сказать: “Я работаю над Android-проектом продукта XYZ”. Вместо того, чтобы перечислять все модули прямо в корне проекта (представьте, что вы открываете проект и пытаетесь найти один из 150+ перечисленных модулей, над которым вы работаете), он был реструктурирован, как показано на рисунке ниже:
Желтый и серые — папки, зеленые — модули. Зеленые стрелки демонстрируют структуру каталогов, соответствующую каждой папке.
Эта структура оказалась довольно гибкой в двух отношениях:
Если команда племени растет, не потребуется каких-либо значительных усилий для разделения модуля на два, если это необходимо.
Если модуль становится слишком большим, его легко разбить на более мелкие модули, сохраняя при этом связную структуру кода.
Как разбить большой модуль и сохранить целостность?
Например, Pix — это метод оплаты, разработанный Центральным банком Бразилии, который позволяет совершать транзакции менее чем за 10 секунд, 24 часа в сутки, семь дней в неделю, включая выходные и праздничные дни.
Модуль Pix в будущем нужно будет разделить на несколько других модулей, так как Центральный Банк регулярно выпускает обновления разного рода. С развитием этого функционала структура могла бы выглядеть примерно так:
Пунктирные линии указывают, что модули, обозначенные зеленым цветом, являются частью каталога, обозначенного желтым цветом.
В идеале дальнейшая разбивка на модули до этого уровня должна происходить только в том случае, если это необходимо или если этот модуль начинает влиять на время сборки.
Модули виджетов (Widget) предназначены для изоляции частей функционала, используемых другими функциональными модулями. Например, домашняя страница приложения отображает точку входа для доступа к Pix. Таким образом, вместо того, чтобы напрямую зависеть от постоянно меняющегося модуля, который никогда не сможет использовать кэш Gradle, модуль домашней страницы становится зависимым от модуля с точечными изменениями, что позволяет поддерживать время сборки в разумный рамках.
Очень важно сформировать структуру, направленную на то, чтобы зависимости между модулями были как можно более прямолинейными. Обратите внимание, что виджеты избегают прямой связи между модулями с разными фичами.
Архитектура модуля Shared изолирует весь общий с другими модулями того же функционала или продукта код. Таким образом, нет необходимости создавать несколько модифицированных интерфейсов или дублировать несколько шаблонов, поскольку они имеют общий домен.
Модуль Shared позволяет избежать разделения на слои, так как такой модуль содержит все, от пользовательского интерфейса до слоя ввода и вывода данных. Обратите внимание, что модуль Shared избегает прямой связи между модулями той же фичи, как и в случае с Pix.
Пунктирные линии показывают, что модуль, выделенный зеленым цветом, представляет собой каталог в папке Pix, выделенной желтым цветом.
Shared и widget — это номенклатуры, определенные для совместного использования кода в рамках проекта и поддержания четкого понимания командой того, для чего предназначен каждый модуль. Здесь важно определить максимально прозрачные отношения между модулями.
Структурирование (создание модулей, перемещение папок и т. д.) требует обновления файла settings.gradle и всех затронутых импортируемых модулей в файлах build.gradle. Имейте в виду, что это может занять какое-то время.
Поддержание работоспособности проекта
Не смотря на то, что в настоящее время наш проект настолько обширен, правки в него мы вносим практически каждый день. Вот несколько вещей которые могут с этим помочь:
Удалите неиспользуемые зависимости.
Пересмотр структуры проекта очень полезен.
Следите, чтобы граф Gradle был не слишком глубоким.
Добавьте файл readme в корень модуля с описанием его бизнес-правил.
Предпочитайте композицию наследованию (в этом проекте нет базового класса (BaseActivity, BaseFragment, BaseXYZ), ведь это упрощает модуляризацию.
В заключение
Не переусердствуйте с количество модулей. Лучше потратьте время на создание хороших абстракций, даже если все они будут в одном месте. Со временем они очень пригодятся для модуляризации.
Избегайте крайностей в своей архитектуре и помните, что ценность продукта заключается не только в количестве имеющихся в нем модулей, но и в том, как приложение улучшает повседневную работу клиента.
Следите за тем, как должна развиваться структура проекта, обращайте внимание на культуру и структуру компании, в которой вы работаете, а не просто следуйте за хайпом в мире разработки.
Техническое и деловое видение приносит пользу и позволит проекту достигнуть зрелости.
Если у вас остались вопросы, вы можете найти меня в Mastodon и Twitter, где я с удовольствием отвечу на них.
Огромное спасибо Леонардо Пайшао (Leonardo Paixão), Брено Крузу (Breno Cruz), Аллану Хасэгава (Allan Hasegawa) и Тьяго Оливейра (Thiago Oliveira) за технический обзор и советы.
Материал подготовлен в преддверии старта специализации Android-разработчик. Узнать подробнее о курсе и зарегистрироваться на бесплатный урок можно по ссылке ниже.