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

Почему Repository находится в Domain-слое?

1.6 Junior🔥 171 комментариев
#Опыт и софт-скиллы#Работа с данными

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

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

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

Repository в Domain Layer: абстракция, принципы и цели

В классической чистой архитектуре (Clean Architecture), предложенной Робертом Мартином, и её адаптации для Android (например, в подходе MAD Skills или Guide to App Architecture от Google), Repository является ключевым элементом Domain Layer (или слоя бизнес-логики). Это не случайное размещение, а фундаментальное архитектурное решение, основанное на нескольких важных принципах.

1. Основная роль Repository: абстракция источника данных

Repository — это абстрактный интерфейс, который определяет контракты (методы) для получения и сохранения данных, необходимых бизнес-логике приложения. Его основная цель — скрыть конкретные детали реализации работы с данными (сеть, локальная база данных, память, файлы) от бизнес-правил и Use Cases.

// Пример интерфейса Repository в Domain слое
interface UserRepository {
    suspend fun getUserById(id: String): User
    suspend fun saveUser(user: User)
    fun observeUsers(): Flow<List<User>>
}

Этот интерфейс не зависит от фреймворков, библиотек или конкретных технологий (например, Room, Retrofit, Firebase). Он говорит только о том, что нужно сделать (получить пользователя), но не как это сделать.

2. Принцип инверсии зависимостей (Dependency Inversion Principle)

Это один из ключевых принципов SOLID, который напрямую применяется здесь.

  • Domain Layer (высокоуровневые бизнес-правила) должен не зависеть от Data Layer (низкоуровневых деталей, таких как конкретные API или базы данных).
  • Интерфейс Repository принадлежит Domain Layer и определяет, что нужно бизнес. Его реализация (например, UserRepositoryImpl) находится в Data Layer и знает, как это сделать (через REST API или SQL запрос).
  • Таким образом, зависимость направлена от низкоуровневых модулей к высокоуровневым абстракциям, что делает систему гибкой и тестируемой.
// UseCase (Domain Layer) зависит только от абстракции.
class GetUserUseCase(
    private val userRepository: UserRepository // Абстрактный интерфейс!
) {
    suspend fun execute(id: String): User = userRepository.getUserById(id)
}

// Реализация (Data Layer) зависит от интерфейса Domain Layer.
class UserRepositoryImpl(
    private val localDataSource: LocalDataSource,
    private val remoteDataSource: RemoteDataSource
) : UserRepository { // Конкретная реализация абстракции
    override suspend fun getUserById(id: String): User {
        // Здесь знаем "как": сначала проверяем локально, потом сеть, etc.
        ...
    }
}

3. Централизация и упрощение бизнес-логики

Бизнес-правила (Use Cases, Domain Models) часто требуют данных из нескольких источников. Например, "показать список задач": нужно проверить локальный кэш, при необходимости запросить из сети, объединить результаты, возможно, обработать конфликты. Если Use Case напрямую обращается к RoomDao и RetrofitService, его код становится сложным, замусоренным техническими деталями и трудным для тестирования.

Repository решает эту проблему, выступая как единая точка доступа к данным для Domain Layer. Он берет на себя ответственность за стратегии кэширования, выбор источника данных и преобразование сырых данных (DTO из сети, Entity из базы) в единый Domain Model, который понимает бизнес-логика.

4. Тестирование (Unit Tests)

Размещение интерфейса в Domain Layer позволяет легко тестировать бизнеслогику (Use Cases) без реальной базы данных или сети. Для этого используется Mock или Fake реализация Repository, возвращающая заранее подготовленные данные.

// Тест UseCase с Fake Repository
class FakeUserRepository : UserRepository {
    private val fakeUsers = mutableMapOf<String, User>()

    override suspend fun getUserById(id: String): User {
        return fakeUsers[id] ?: throw NotFoundException()
    }
}

@Test
fun `execute should return user when exists`() = runTest {
    val fakeRepo = FakeUserRepository()
    fakeRepo.saveUser(User("test-id", "Test Name"))
    val useCase = GetUserUseCase(fakeRepo)

    val result = useCase.execute("test-id")

    assertEquals("Test Name", result.name)
}

5. Изоляция изменений и поддержка гибкости

Если потребуется заменить источник данных (например, перейти от Firebase к собственному REST API), или добавить новую стратегию кэширования, изменения будут локализованы только в Data Layer — в конкретной реализации RepositoryImpl. Domain Layer (Use Cases) и Presentation Layer (ViewModel) останутся абсолютно неизменными, потому что они работают только через абстрактный интерфейс. Это снижает риски, стоимость изменений и предотвращает "распространение" технических деталей по всему приложению.

Итог

Repository находится в Domain Layer как абстрактный интерфейс по следующим фундаментальным причинам:

  • Реализация принципа инверсии зависимостей: высокоуровневая бизнес-логика не зависит от низкоуровневых деталей данных.
  • Предоставление чистой абстракции для Use Cases, освобождая их от технических деталей работы с данными.
  • Создание единой точки доступа и централизация сложных операций с данными (кэширование, объединение источников).
  • Обеспечение легкого и эффективного модульного тестирования бизнес-логики.
  • Изоляция изменений в инфраструктуре данных, повышая гибкость и устойчивость архитектуры приложения к эволюции.

Таким образом, это не просто "класс для работы с данными", а критически важная архитектурная граница, которая защищает ядро бизнес-логики приложения от изменений во внешнем мире и сложности инфраструктуры.