Сегодня я хочу познакомить вас с вариантом построения многомодульной архитектуры под Android. Но сначала обязательно вспомним про понятие Clean Architecture и для чего вообще надо задумываться об архитектуре вашего кода.
Зачем нужна архитектура?
Какую цель преследует тот, кто когда-либо писал или читал чьи-то доклады по разработке ПО? Обычно это одно из двух:
либо человек хочет решить какую-то конкретную прикладную проблему,
либо он хочет, чтобы его продукт был лучше.
Так вот, что же это такое «лучше»? Последнее время этот вопрос всё чаще всплывает в контексте архитектуры программного продукта. Ведь мощная базовая архитектура — важный показатель для масштабируемости приложения. Внесение любых изменений в проект может потребовать переписать приложение практически полностью. В таких случаях код тесно связан. Использование Чистой архитектуры (Clean architecture) помогает решить эту проблему.
![](https://habrastorage.org/getpro/habr/upload_files/d60/fde/7d8/d60fde7d8b4ff2df1f81a5553da881dd.png)
Это одно из самых популярных и часто используемых решений для крупных приложений с большим количеством функций и SOLID-принципами. Подход был предложен Робертом С. Мартином (известным как Дядя Боб) в блоге «Чистый код» в 2012 году.
Всем известная схема, которую также иногда называют «Луковицей».
![Источник: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html Источник: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html](https://habrastorage.org/getpro/habr/upload_files/9c8/71a/51a/9c871a51a89f715fc2ba0bf51445d2fe.png)
Многие распространённые архитектуры, такие как MVC, MVP, MVVM, MVI по сути являются вариантами подхода Сlean architecture. Они могут выглядеть запутанно, но разобравшись однажды со всей внутрянкой чистой архитектуры, в дальнейшем у вас не возникнет проблем.
Итак, зачем нужен чистый подход?
Принципы чистой архитектуры
![](https://habrastorage.org/getpro/habr/upload_files/e2c/1e5/6f9/e2c1e56f9b07be9ef22c604c1566eb7c.png)
В основе чистой архитектуры лежит идея разделения кода на слои. Давайте бросим взгляд них.
![](https://habrastorage.org/getpro/habr/upload_files/e36/a9b/088/e36a9b088352e7757546d03c80c8c01e.png)
Domain-слой: Запускает независимую от других уровней бизнес-логику. В идеале — это чистый пакет Kotlin без android-зависимостей.
Data-слой: Отправляет необходимые для приложения данные в domain-слой, реализуя предоставляемый доменом интерфейс.
Presentation-слой: Включает в себя как domain-, так и data-слои, а также является специфическим для android и выполняет UI-логику.
Исходя из данных принципов, как правило, в проектах делают так: выделяют какие-то общие модули, а остальное организуют одним из двух вариантов:
Код содержат в общем модуле application, при этом внутри делят по слоям на пакеты: Domain — Data — Ui.
Заводят модуль application и какое-то количество модулей feature, внутри каждого из которых также имеются пакеты с тремя слоями.
Давайте также обозначим, что здесь имеется в виду под «feature» — это логически законченный, максимально независимый модуль программы, решающий конкретную пользовательскую проблему, с чётко обозначенными внешними зависимостями, и который относительно легко переиспользовать в другой программе. Одно из ключевых выражений в определении фичи — это «с чётко обозначенными внешними зависимостями».
Поэтому давайте всё, что мы хотим от внешнего мира для фичи, будем описывать в специальном интерфейсе, например:
![](https://habrastorage.org/getpro/habr/upload_files/b47/e2b/37d/b47e2b37d539ee24731eea85b5dbfa56.png)
![](https://habrastorage.org/getpro/habr/upload_files/0e5/e79/b26/0e5e79b267ade6dc61a47283ab676c77.png)
Здесь мы говорим о некой фиче, связанной с процессом авторизации. В данном случае мы через интерфейс LoginDataDependencies обозначаем внешние зависимости для модуля фичи.
Другая важная составляющая «чистой» фичи — это наличие чёткого API, по которому внешний мир может обращаться к фиче.
![](https://habrastorage.org/getpro/habr/upload_files/a52/df3/003/a52df300301e76957318aa5cb14a8d78.png)
Интерфейс доменного слоя предоставляет для остальных модулей чёткое API — репозиторий со своей функциональностью. Ниже приведён пример включения данного компонента в контексте Dagger2.
![](https://habrastorage.org/getpro/habr/upload_files/eb9/3eb/a5f/eb93eba5ff2a47d99e992a1645a14171.png)
Здесь я буду приводить примеры кода с использованием Dagger2.
Конечно, эти подходы и принципы не исчерпывающие. Ни один принцип не может быть универсальным и подходить для решения любой задачи. Плюс всегда хочется улучшать решения, либо просто поэкспериментировать. Таким образом рождаются новые решения: какие-то подходят для узких кейсов, а какие-то выходят более универсальными. И когда человек знаком с различными вариантами решений — это уже половина успеха, ведь он знает, как другие люди решали подобные задачи и всегда может воспользоваться готовым решением, адаптировав его под себя.
Я хотела бы познакомить вас с нашим решением, которое лично для меня стало чем-то новым. До этого меня жизнь не сталкивала и с Dagger 2. Но основным инсайтом для меня стал принцип, который работает совместно с этим фреймворком — я говорю о варианте многомодульной архитектуры приложения Андроид.
Рецепт Оливье
![](https://habrastorage.org/getpro/habr/upload_files/e57/26f/340/e5726f3407bcc0e6da1156acd9dce378.png)
Суть идеи, которая отличает её от тех решений, что уже известны мне на текущий момент, в том, что каждую feature нужно делить на три модуля: «Data — Domain — Presentation».
Таким образом, если в проекте, допустим, 10 feature, то у нас будет минимум 30 модулей. Почему минимум? Потому что наверняка у каждого в проекте есть какие-то common модули, utils или разделяемые модули, которые мы используем в нескольких проектах.
![](https://habrastorage.org/getpro/habr/upload_files/7f5/823/7d0/7f58237d079fe0c361ed1ab4e7508503.png)
Минусы подхода
![](https://habrastorage.org/getpro/habr/upload_files/cfe/590/432/cfe590432483beed7afab1ac97dbfb7e.png)
После озвучивания варианта многомодульной архитектуры, сразу видны два основных недостатка данного подхода. Для каких-то проектов они будут неприемлемы, но в нашем случае их серьёзность нивелирована плюсами при использовании.
Теперь давайте более подробно рассмотрим, о чём же идёт речь, с примерами, картинками и т.д.
Но сначала задумайтесь:
![](https://habrastorage.org/getpro/habr/upload_files/2be/fa9/78c/2befa978c3244039276b3c98df80ab17.png)
Первое, что мне приходит в голову — это логин, и дальнейшая работа с токеном авторизации во всем проекте. Я уверена, что каждый из вас может придумать миллион подобных кейсов, и на любом проекте такая ситуация вряд ли будет единичной. При этом если у вас всё делится по фичам, то, скорее всего, на примере логина вам пришлось бы тянуть весь модуль логина вместе с его UI туда, где просто нужна была бы API/репозиторий логина в какой-то другой модуль. И тогда встаёт вопрос — а зачем нам тянуть это всё, и реализацию с мапперами, ретрофитом, конвертором, который вы используете для json, если нам нужны лишь пара доменных моделей и API? Именно такой кейс и стал основной причиной разделения каждой фичи на три части.
Другим примером целесообразности деления можно считать скорость инкрементальной сборки при внесении изменений: представьте, что у вас поменялся endpoint у API, а вам нужно будет пересобирать весь модуль фичи, с UI и прочими штуками? А если у вас там есть какие-то тяжеловесные компоненты? Но если разделить на три части каждую фичу, то при внесении изменений пересобрать придётся только один из трёх модулей! Это же круто! А если у вас при этом модуль ещё тянулся в других фичах, они тоже должны пересобираться. В общем, по цепочке может пойти всё довольно далеко.
Да, при этом у нас множится количество модулей — их становится в три раза больше. Но скорость сборки проекта у нас при этом не сильно страдает. Цифрами я тут вас, к сожалению, не порадую, но лид проекта клялся на Котлине, что на предыдущем проекте имел более сотни модулей и сборка не улетала в космос. Поверим ему.
Содержимое модулей
Далее пробежимся по краткому содержанию каждого из модулей и иллюстрациями соответствующих пакетов в проекте.
![](https://habrastorage.org/getpro/habr/upload_files/9e3/8a8/31b/9e38a831b202eef608abd7a489ac80d4.png)
В доменном слое у нас находятся абстракции:
корневые/базовые модели (модели бизнес-логики, которые никак не связаны с сетью и отображением на Ui) — data /sealed classes;
интерфейс репозитория.
В данном модуле DI не нужен, а в остальных модулях — Data и Ui — у нас, естественно, будет Dagger.
![](https://habrastorage.org/getpro/habr/upload_files/bb8/a05/ecf/bb8a05ecf5aa2255f28db12c6c6a4624.png)
В дата слое у нас лежат:
DTO модели (те модели, которые нам будут приходить по сетевому слою);
API;
реализации репозиториев;
мапперы из DTO в доменные модели, какие-то дополнительные мапперы, возможно;
DI из 3 частей:
Module — для провайда тех сущностей, которые в данном слое будут использоваться;
Component, в котором указан наш модуль и Dependency;
Dependency модуля (как минимум, это baseUrl для API, скорее всего, различные common провайдеры — клиент Retrofit, парсера для Json вроде Moshi. Возможно, логин/токен providers — всё, что требуется в качестве внешних зависимостей для текущего модуля).
![](https://habrastorage.org/getpro/habr/upload_files/a3c/081/f5a/a3c081f5abcfb2b595d7bc485452dae6.png)
Что касается UI модуля: он у каждой фичи будет свой особенный, но некая общая структура у всех UI модулей, конечно, имеется. Давайте расскажу о ней подробнее.
DI из 4 частей:
Module;
Component;
(опционально) Dependency модуля;
UiChildComponentProvider — если внутри фичи есть хотя бы один экран, который имеет свой DI, то вводим данный интерфейс, который поможет нам работать с «главным экраном» или с «хостовым экраном», о которых я расскажу подробнее чуть дальше. В любом случае он будет реализовывать функции:
fun provide(module: FeatureDataModule): FeatureDataComponent
у каждого экрана/фрагмента будет также свой набор DI — модуль, компонент и компонент-провайдер.
Чтобы сориентироваться, давайте бросим взгляд на иерархию зависимостей / вложенностей в рамках Dagger в данном случае:
![](https://habrastorage.org/getpro/habr/upload_files/e49/c3f/04e/e49c3f04e7455d05e6af55831df8591d.png)
И в качестве примера представим, как может выглядеть экран по фрагментам / экранам:
Первый — это экран, открываемый из меню. На нём лежит «основной» фрагмент фичи, в котором есть несколько табов. В каждом табе — дочерние фрагменты со списком элементов, по клику на которые мы переходим в некие «подробности элемента».
Второй экран — «подробности». Такой экран в моём примере организован как HostFragment: он представляет из себя единую точку входа для внутреннего контента, на котором располагается/подменяется внутреннее содержимое. В качестве первого показываемого экрана (а вы помните, тут может быть и некий flow) — фрагмент с ещё одним набором табов (допустим, разделение по разным категориям информации об элементе).
Вывод
Итак, зачем нужна модуляризация в приложении?
![](https://habrastorage.org/getpro/habr/upload_files/43f/273/5d1/43f2735d1ca4ee9ff4c90f0beebb57ea.png)
Масштабирование разработки. Подход позволяет горизонтально расширять отдел разработки без особых трудностей: новые сотрудники занимаются новыми модулями в изоляции.
Экономия времени и ресурсов. Когда вам понадобится переиспользовать код в другом продукте, вы сразу оцените скорость, с которой можно это сделать.
Синергия между приложениями. Продуктовые запросы обогащают модули функциональностью, которая может быть использована во всех продуктах компании.
Качество кода. Как уже говорилось выше, когда модули специализированные, а их интерфейс прост, связность кода становится существенно ниже, как и порог вхождения в проект новых программистов. Также упрощаются поддержка и тестирование кода.
А если я всё ещё вас не убедила перейти в своём проекте на такой вариант архитектуры, то вы можете просто попробовать проектировать так те свои фичи, которые наверняка будут переиспользоваться в других модулях / фичах, чтобы снизить скорость сборки проекта при внесении изменений , применить переиспользование слоёв и устроить хорошее разделение ответственности.
ChPr
Зачем дата мапперам разделение на интерфейс и имплементацию? Вы их мокаете в тестах?
Alena_Fox_Spb Автор
Да, задумка была мокать для тестов. Но в целом — это не принципиальное решение, можно делать и другим способом, допустим как экстеншены для дата классов!