Можно ли реализовать Clean Architecture в приложении с одним модулем?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Можно ли реализовать Clean Architecture в одном модуле?
Да, безусловно можно. Чистая архитектура (Clean Architecture) — это прежде всего архитектурный подход и набор принципов, а не жёсткое предписание по количеству модулей. Ключевая цель — организация кода с чётким разделением ответственности, независимостью бизнес-логики от фреймворков и внешних деталей, а также лёгкостью тестирования. Всё это достижимо и в рамках одного модуля приложения (например, app в Android Studio).
Суть подхода: разделение по слоям, а не по модулям
Основная идея Clean Architecture от Роберта Мартина — это «Слоистая архитектура» с зависимостями, направленными внутрь, к ядру. Классически выделяют три основных круга (слоя):
- Domain Layer (Слой домена / бизнес-логики): Самый внутренний, независимый слой. Содержит Entities (сущности) и Use Cases (интеракторы). Не должен ничего знать о других слоях.
- Data Layer (Слой данных): Реализует контракты (интерфейсы репозиториев), определённые в Domain слое. Содержит Repository Implementations, Data Sources (локальные: Room, DataStore; удалённые: Retrofit), Data Models (DTO).
- Presentation Layer (Слой представления): Включает UI Components (Activity, Fragment, Composable), ViewModels/Presenters, а также UI State Models. Зависит от Domain слоя.
В многомодульном проекте эти слои часто выносят в отдельные модули (:domain, :data, :presentation). Однако в одномудульном проекте мы организуем их в виде чётко выделенных пакетов (packages) и следим за направлением зависимостей между ними.
Практическая реализация в одном модуле
Рассмотрим структуру пакетов для проекта com.example.myapp:
com.example.myapp/
├── domain/
│ ├── model/ # Entity (User, Product)
│ ├── repository/ # Интерфейсы репозиториев (UserRepository)
│ └── usecase/ # Use Cases/Interactors (GetUserUseCase)
├── data/
│ ├── local/ # Room DAO, Database, DataStore
│ ├── remote/ # Retrofit API, модели Network
│ ├── mapper/ # Mappers Data <-> Domain
│ └── repository/ # Реализации репозиториев (UserRepositoryImpl)
└── presentation/
├── ui/ # Activity, Fragment, Composable
├── viewmodel/ # ViewModel (зависит от UseCase)
├── state/ # UiState, UiEvent
└── di/ # Модули Koin/Hilt (опционально)
Ключевой момент: управление зависимостями
Чтобы соблюсти правило «зависимости направлены внутрь», мы используем инверсию зависимостей (Dependency Inversion Principle, DIP). Domain слой определяет абстракции (интерфейсы), которые реализуются во внешних слоях.
Пример:
-
Domain слой объявляет контракт:
// domain/repository/UserRepository.kt interface UserRepository { suspend fun getUser(id: String): User } -
Data слой предоставляет реализацию:
// data/repository/UserRepositoryImpl.kt class UserRepositoryImpl @Inject constructor( private val userApi: UserApi, private val userDao: UserDao ) : UserRepository { override suspend fun getUser(id: String): User { // Логика кэширования, выбор источника данных return userDao.getUser(id)?.toDomain() ?: fetchFromNetwork(id) } private suspend fun fetchFromNetwork(id: String): User { ... } } -
Presentation слой (ViewModel) зависит от абстракции (UseCase), а не от конкретной реализации:
// presentation/viewmodel/UserViewModel.kt @HiltViewModel class UserViewModel @Inject constructor( private val getUserUseCase: GetUserUseCase // UseCase объявлен в Domain ) : ViewModel() { // Использует только бизнес-логику fun loadUser(id: String) { viewModelScope.launch { _uiState.value = UiState.Loading try { val user = getUserUseCase(id) _uiState.value = UiState.Success(user) } catch (e: Exception) { _uiState.value = UiState.Error(e.message) } } } }
Преимущества такого подхода даже в одном модуле
- Тестируемость: Бизнес-логику в
domainиusecaseможно тестировать юнит-тестами в полной изоляции, подменяя зависимости наFakeRepository. - Читаемость и поддерживаемость: Код чётко структурирован. Новому разработчику легко понять, где что искать.
- Гибкость: Замена библиотек (например, с Retrofit на Ktor) или источников данных затрагивает только
dataслой. - Независимость от фреймворка: Бизнес-правила не завязаны на Android SDK, что теоретически позволяет перенести
domainслой в Kotlin Multiplatform проект.
Ограничения одномудульного подхода
- Физическое разделение: Невозможно настройкой
build.gradleзапретитьdomainмодулю зависеть отandroidx.room. Приходится полагаться на дисциплину команды и code review. - Время сборки: При изменении в
domainслое пересобирается весь модуль, а не только зависящие от него. В многомодульном проекте гранулярность пересборки выше. - Повторное использование: Сложнее выделить общую бизнес-логику (
domain) для использования в другом приложении без копирования пакетов.
Итог
Реализация Clean Architecture в одном модуле не только возможна, но и является отличной отправной точкой для большинства проектов среднего размера. Она позволяет внедрить все ключевые принципы: разделение ответственности, инверсию зависимостей и лёгкое тестирование. Переход на многомодульную структуру становится логическим следующим шагом, когда проект вырастает, и потребности в строгой физической изоляции кода, ускорении сборки за счёт параллелизации и кросс-платформенном reuse становятся критичными. Начинать же с одного модуля — это pragmatically верное решение, позволяющее получить преимущества архитектуры без излишнего усложнения процесса разработки на ранних этапах.