Pull to refresh
EPAM
Компания для карьерного и профессионального роста

«Оливье в каждой семье свой», или как мы придумали ещё одну многомодульную архитектуру

Reading time7 min
Views5K

Сегодня я хочу познакомить вас с вариантом построения многомодульной архитектуры под Android. Но сначала обязательно вспомним про понятие Clean Architecture и для чего вообще надо задумываться об архитектуре вашего кода.

Зачем нужна архитектура?

Какую цель преследует тот, кто когда-либо писал или читал чьи-то доклады по разработке ПО? Обычно это одно из двух: 

  • либо человек хочет решить какую-то конкретную прикладную проблему,

  • либо он хочет, чтобы его продукт был лучше. 

Так вот, что же это такое «лучше»? Последнее время этот вопрос всё чаще всплывает в контексте архитектуры программного продукта. Ведь мощная базовая архитектура — важный показатель для масштабируемости приложения. Внесение любых изменений в проект может потребовать переписать приложение практически полностью. В таких случаях код тесно связан. Использование Чистой архитектуры (Clean architecture) помогает решить эту проблему. 

Это одно из самых популярных и часто используемых решений для крупных приложений с большим количеством функций и 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

Многие распространённые архитектуры, такие как MVC, MVP, MVVM, MVI по сути являются вариантами подхода Сlean architecture. Они могут выглядеть запутанно, но разобравшись однажды со всей внутрянкой чистой архитектуры, в дальнейшем у вас не возникнет проблем.

Итак, зачем нужен чистый подход?

Принципы чистой архитектуры

В основе чистой архитектуры лежит идея разделения кода на слои. Давайте бросим взгляд них.  

Domain-слой: Запускает независимую от других уровней бизнес-логику. В идеале — это чистый пакет Kotlin без android-зависимостей.

Data-слой: Отправляет необходимые для приложения данные в domain-слой, реализуя предоставляемый доменом интерфейс.

Presentation-слой: Включает в себя как domain-, так и data-слои, а также является специфическим для android и выполняет UI-логику.

Исходя из данных принципов, как правило, в проектах делают так: выделяют какие-то общие модули, а остальное организуют одним из двух вариантов:

  1. Код содержат в общем модуле application, при этом внутри делят по слоям на пакеты: Domain Data Ui.

  2. Заводят модуль application и какое-то количество модулей feature, внутри каждого из которых также имеются пакеты с тремя слоями. 

Давайте также обозначим, что здесь имеется в виду под «feature» — это логически законченный, максимально независимый модуль программы, решающий конкретную пользовательскую проблему, с чётко обозначенными внешними зависимостями, и который относительно легко переиспользовать в другой программе. Одно из ключевых выражений в определении фичи — это «с чётко обозначенными внешними зависимостями». 

Поэтому давайте всё, что мы хотим от внешнего мира для фичи, будем описывать в специальном интерфейсе, например:

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

Другая важная составляющая «чистой» фичи — это наличие чёткого API, по которому внешний мир может обращаться к фиче.

Интерфейс  доменного слоя предоставляет для остальных модулей чёткое API — репозиторий со своей функциональностью.  Ниже приведён пример включения данного компонента в контексте Dagger2. 

Здесь я буду приводить примеры кода с использованием Dagger2.

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

Я хотела бы познакомить вас с нашим решением, которое лично для меня стало чем-то новым. До этого меня жизнь не сталкивала и с Dagger 2. Но основным инсайтом для меня стал принцип, который работает совместно с этим фреймворком я говорю о варианте многомодульной архитектуры приложения Андроид.

Рецепт Оливье

Суть идеи, которая отличает её от тех решений, что уже известны мне на текущий момент, в том, что каждую feature нужно делить на три модуля: «Data Domain Presentation»

Таким образом, если в проекте, допустим, 10 feature, то у нас будет минимум 30 модулей. Почему минимум? Потому что наверняка у каждого в проекте есть какие-то common модули, utils или разделяемые модули, которые мы используем в нескольких проектах. 

Минусы подхода 

После озвучивания варианта многомодульной архитектуры, сразу видны два основных недостатка данного подхода. Для каких-то проектов они будут неприемлемы, но в нашем случае их серьёзность нивелирована плюсами при использовании.

Теперь давайте более подробно рассмотрим, о чём же идёт речь, с примерами, картинками и т.д. 

Но сначала задумайтесь:

Первое, что мне приходит в голову — это логин, и дальнейшая работа с токеном авторизации во всем проекте. Я уверена, что каждый из вас может придумать миллион подобных кейсов, и на любом проекте такая ситуация вряд ли будет единичной. При этом если у вас всё делится по фичам, то, скорее всего, на примере логина вам пришлось бы тянуть весь модуль логина вместе с его UI туда, где просто нужна была бы API/репозиторий логина в какой-то другой модуль. И тогда встаёт вопрос — а зачем нам тянуть это всё, и реализацию с мапперами, ретрофитом, конвертором, который вы используете для json, если нам нужны лишь пара доменных моделей и API? Именно такой кейс и стал основной причиной разделения каждой фичи на три части. 

Другим примером целесообразности деления можно считать скорость инкрементальной сборки при внесении изменений: представьте, что у вас поменялся endpoint у API, а вам нужно будет пересобирать весь модуль фичи, с UI и прочими штуками? А если у вас там есть какие-то тяжеловесные компоненты? Но если разделить на три части каждую фичу, то при внесении изменений пересобрать придётся только один из трёх модулей! Это же круто! А если у вас при этом модуль ещё тянулся в других фичах, они тоже должны пересобираться. В общем, по цепочке может пойти всё довольно далеко. 

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

Содержимое модулей

Далее пробежимся по краткому содержанию каждого из модулей и иллюстрациями соответствующих пакетов в проекте.

В доменном слое у нас находятся абстракции: 

  • корневые/базовые модели (модели бизнес-логики, которые никак не связаны с сетью и отображением на Ui) — data /sealed classes;

  • интерфейс репозитория.

В данном модуле DI не нужен, а в остальных модулях — Data и Ui — у нас, естественно, будет Dagger. 

В дата слое у нас лежат:

  • DTO модели (те модели, которые нам будут приходить по сетевому слою);

  • API; 

  • реализации репозиториев;

  • мапперы из DTO в доменные модели, какие-то дополнительные мапперы, возможно;

  • DI из 3 частей: 

    • Module — для провайда тех сущностей, которые в данном слое будут использоваться;

    • Component, в котором указан наш модуль и Dependency;

    • Dependency модуля (как минимум, это baseUrl для API, скорее всего, различные common провайдеры — клиент Retrofit, парсера для Json вроде Moshi. Возможно, логин/токен providers — всё, что требуется в качестве внешних зависимостей для текущего модуля).

Что касается UI модуля: он у каждой фичи будет свой особенный, но некая общая структура у всех UI модулей, конечно, имеется. Давайте расскажу о ней подробнее. 

  • DI из 4 частей: 

    • Module; 

    • Component; 

    • (опционально) Dependency модуля; 

    • UiChildComponentProvider — если внутри фичи есть хотя бы один экран, который имеет свой DI, то вводим данный интерфейс, который поможет нам работать с «главным экраном» или с «хостовым экраном», о которых я расскажу подробнее чуть дальше. В любом случае он будет реализовывать функции:

fun provide(module: FeatureDataModule): FeatureDataComponent

  • у каждого экрана/фрагмента будет также свой набор DI — модуль, компонент и компонент-провайдер.

Чтобы сориентироваться, давайте бросим взгляд на иерархию зависимостей / вложенностей в рамках Dagger в данном случае:

И в качестве примера  представим, как может выглядеть экран по фрагментам / экранам:

  • Первый — это экран, открываемый из меню. На нём лежит «основной» фрагмент фичи, в котором есть несколько табов. В каждом табе —  дочерние фрагменты со списком элементов, по клику на которые мы переходим в некие «подробности элемента».

  • Второй экран — «подробности». Такой экран в моём примере организован как HostFragment: он представляет из себя единую точку входа для внутреннего контента,  на котором располагается/подменяется внутреннее содержимое. В качестве первого показываемого экрана (а вы помните, тут может быть и некий flow) — фрагмент с ещё одним набором табов (допустим, разделение по разным категориям информации об элементе).

Вывод

Итак, зачем нужна модуляризация в приложении?

  1. Масштабирование разработки. Подход позволяет горизонтально расширять отдел разработки без особых трудностей: новые сотрудники занимаются новыми модулями в изоляции. 

  2. Экономия времени и ресурсов. Когда вам понадобится переиспользовать код в другом продукте, вы сразу оцените скорость, с которой можно это сделать.

  3. Синергия между приложениями. Продуктовые запросы обогащают модули функциональностью, которая может быть использована во всех продуктах компании.

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

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

Tags:
Hubs:
Total votes 9: ↑2 and ↓7-5
Comments2

Articles

Information

Website
www.epam.com
Registered
Founded
1993
Employees
over 10,000 employees
Location
США
Representative
vesyolkinaolga