← Назад к вопросам

Можно ли реализовать Clean Architecture в приложении с одним модулем?

2.0 Middle🔥 151 комментариев
#Архитектура и паттерны

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

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Можно ли реализовать Clean Architecture в одном модуле?

Да, безусловно можно. Чистая архитектура (Clean Architecture) — это прежде всего архитектурный подход и набор принципов, а не жёсткое предписание по количеству модулей. Ключевая цель — организация кода с чётким разделением ответственности, независимостью бизнес-логики от фреймворков и внешних деталей, а также лёгкостью тестирования. Всё это достижимо и в рамках одного модуля приложения (например, app в Android Studio).

Суть подхода: разделение по слоям, а не по модулям

Основная идея Clean Architecture от Роберта Мартина — это «Слоистая архитектура» с зависимостями, направленными внутрь, к ядру. Классически выделяют три основных круга (слоя):

  1. Domain Layer (Слой домена / бизнес-логики): Самый внутренний, независимый слой. Содержит Entities (сущности) и Use Cases (интеракторы). Не должен ничего знать о других слоях.
  2. Data Layer (Слой данных): Реализует контракты (интерфейсы репозиториев), определённые в Domain слое. Содержит Repository Implementations, Data Sources (локальные: Room, DataStore; удалённые: Retrofit), Data Models (DTO).
  3. 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 слой определяет абстракции (интерфейсы), которые реализуются во внешних слоях.

Пример:

  1. Domain слой объявляет контракт:

    // domain/repository/UserRepository.kt
    interface UserRepository {
        suspend fun getUser(id: String): User
    }
    
  2. 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 { ... }
    }
    
  3. 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 верное решение, позволяющее получить преимущества архитектуры без излишнего усложнения процесса разработки на ранних этапах.

Можно ли реализовать Clean Architecture в приложении с одним модулем? | PrepBro