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

Как реализуется принцип Dependency Inversion в Clean Architecture?

3.0 Senior🔥 151 комментариев
#Dependency Injection#Архитектура и паттерны

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

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

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

Как реализуется Dependency Inversion в Clean Architecture?

Dependency Inversion (DI) — один из 5 принципов SOLID. Суть: высокоуровневые модули НЕ должны зависеть от низкоуровневых, оба должны зависеть от абстракций.

1. Проблема: Tight Coupling (неправильно)

// НЕПРАВИЛЬНО: прямая зависимость
class UserRepository {
    fun getUser(id: Int): User {
        val response = HttpClient.get("/users/$id")
        return parseUser(response)
    }
}

class UserViewModel(private val repo: UserRepository) {
    fun loadUser(id: Int) {
        val user = repo.getUser(id)
    }
}

// Проблемы:
// - Сложно тестировать (нельзя подменить HttpClient)
// - Сложно менять реализацию (заменить API на БД)
// - Высокая связанность слоёв

2. Решение: Dependency Inversion через интерфейсы

// ПРАВИЛЬНО: через интерфейсы

// 1. Абстракция (контракт)
interface UserDataSource {
    suspend fun getUser(id: Int): User
}

// 2. Реализация (конкретная)
class ApiUserDataSource : UserDataSource {
    override suspend fun getUser(id: Int): User {
        val response = httpClient.get("/users/$id")
        return parseUser(response)
    }
}

// 3. Repository зависит от ИНТЕРФЕЙСА
class UserRepository(
    private val dataSource: UserDataSource
) {
    suspend fun getUser(id: Int): User = dataSource.getUser(id)
}

// 4. ViewModel зависит от ИНТЕРФЕЙСА
class UserViewModel(
    private val repo: UserRepository
) {
    fun loadUser(id: Int) {
        val user = repo.getUser(id)
    }
}

3. Архитектурные слои Clean Architecture

Presentation (UI)
   ↓ зависит от Application
Application (Use Cases)
   ↓ зависит от Domain
Domain (Entities, Interfaces)
   ↑ реализует Infrastructure
Infrastructure (Repository, API, Database)

4. Полный пример Clean Architecture

// DOMAIN (правила бизнеса)
data class User(
    val id: Int,
    val name: String,
    val email: String
)

interface UserRepository {
    suspend fun getUser(id: Int): User
    suspend fun saveUser(user: User)
}

// APPLICATION (use cases)
class GetUserUseCase(
    private val repository: UserRepository
) {
    suspend operator fun invoke(userId: Int): User {
        return repository.getUser(userId)
    }
}

// INFRASTRUCTURE (реальная реализация)
class ApiUserRepository(
    private val httpClient: HttpClient
) : UserRepository {
    override suspend fun getUser(id: Int): User {
        val response = httpClient.get("/users/$id")
        return User(
            id = response["id"].asInt,
            name = response["name"].asString,
            email = response["email"].asString
        )
    }
    
    override suspend fun saveUser(user: User) {
        httpClient.post("/users", user)
    }
}

// PRESENTATION (UI)
class UserViewModel(
    private val getUser: GetUserUseCase
) : ViewModel() {
    private val _user = MutableLiveData<User>()
    val user: LiveData<User> = _user
    
    fun loadUser(id: Int) {
        viewModelScope.launch {
            _user.value = getUser(id)
        }
    }
}

5. Dependency Injection: как передать зависимости?

Способ 1: Hilt (рекомендуется)

// 1. Определить в Module
module
object RepositoryModule {
    fun provideUserRepository(
        httpClient: HttpClient
    ): UserRepository {
        return ApiUserRepository(httpClient)
    }
}

// 2. Использовать в ViewModel
class UserViewModel constructor(
    private val getUser: GetUserUseCase
) : ViewModel() { ... }

// 3. Использовать в Activity
class UserActivity : AppCompatActivity() {
    private val viewModel: UserViewModel by viewModels()
}

Способ 2: Manual (простой случай)

val httpClient = HttpClient()
val repository = ApiUserRepository(httpClient)
val getUser = GetUserUseCase(repository)
val viewModel = UserViewModel(getUser)

6. Инверсия: слои зависят от абстракций

// НЕПРАВИЛЬНО: слой зависит от конкретной реализации
class UserViewModel {
    private val repository = ApiUserRepository()
}

// ПРАВИЛЬНО: слой зависит от интерфейса
class UserViewModel(
    private val repository: UserRepository
) {
    // Зависит от интерфейса, не от конкретной реализации
}

7. Тестирование с DI (главное преимущество)

// Определить mock-реализацию
class MockUserRepository : UserRepository {
    override suspend fun getUser(id: Int): User {
        return User(id, "Test User", "test@example.com")
    }
    
    override suspend fun saveUser(user: User) {}
}

// Тестировать с mock'ом
class UserViewModelTest {
    fun testLoadUser() {
        val mockRepository = MockUserRepository()
        val getUser = GetUserUseCase(mockRepository)
        val viewModel = UserViewModel(getUser)
        
        viewModel.loadUser(1)
        
        assertEquals("Test User", viewModel.user.value?.name)
    }
}

8. Правило зависимостей

  1. Domain НЕ зависит ни от чего
  2. Application зависит только от Domain
  3. Infrastructure реализует интерфейсы из Domain
  4. Presentation зависит от Application
  5. ВСЕ зависят от интерфейсов, а не от конкретных реализаций

9. Результаты DI + Clean Architecture

Тестируемость: подменяешь интерфейсы на mock'и Гибкость: меняешь реализацию без изменения слоёв выше Масштабируемость: легко добавлять новые фичи Чистота кода: слои не перемешаны

Итог

  • DI = Dependency Injection через конструктор
  • Зависимости от интерфейсов, а не конкретных классов
  • Инверсия: слои зависят от Domain, а не наоборот
  • Hilt для автоматизации
  • Главное преимущество: простое тестирование и гибкая архитектура