DIP: основи, інверсія залежностей у розробці

Автор: IT Sectr Опубліковано: 2026-05-12 Час читання: 9 хв

DIP (Dependency Inversion Principle) — п'ятий принцип SOLID, який визначає правила побудови залежностей між модулями: модулі верхнього рівня не повинні залежати від модулів нижнього рівня, обидва повинні залежати від абстракцій. Абстракції не повинні залежати від деталей — деталі повинні залежати від абстракцій. Цей принцип, описаний Робертом Мартіном у Clean Architecture (2017), лежить в основі слабкозв'язаної архітектури. Згідно з цією книгою, принцип інверсії залежностей усуває жорсткі зв'язки між шарами додатка.

Головне

  • DIP — принцип інверсії залежностей, п'ятий у SOLID, про архітектурні межі
  • Модулі верхнього рівня не повинні імпортувати модулі нижнього рівня — лише абстракції
  • DIP ≠ DI: Dependency Inversion — архітектурний принцип, Dependency Injection — спосіб його реалізації
  • Абстракції належать модулю верхнього рівня, а реалізації — нижнього
  • DIP перевертає традиційну ієрархію залежностей у багатошарових архітектурах

Що таке DIP (Dependency Inversion Principle)?

DIP (Dependency Inversion Principle) — принцип інверсії залежностей, який перевертає традиційне уявлення про напрям залежностей між модулями. Високорівневі модулі (бізнес-логіка) не повинні безпосередньо залежати від низькорівневих модулів (база даних, мережа, UI). Натомість обидва рівні залежать від абстракцій, визначених у модулі верхнього рівня.

Формальне формулювання DIP включає два правила: А — модулі верхнього рівня не повинні залежати від модулів нижнього рівня, обидва повинні залежати від абстракцій. Б — абстракції не повинні залежати від деталей, деталі повинні залежати від абстракцій. Друге правило є наслідком першого: якщо абстракція залежить від деталей, вона не може бути стабільною основою для модуля верхнього рівня.

Без DIP типова архітектура виглядає так: BusinessLogic → DatabaseRepository — бізнес-логіка безпосередньо залежить від конкретного репозиторію. З DIP: BusinessLogic → DatabaseServiceInterface ← DatabaseRepository. BusinessLogic не знає про існування DatabaseRepository, він знає лише інтерфейс DatabaseService, який реалізується поза бізнес-логікою.

Напрям залежностей у DIP

Інверсія означає, що потік керування та потік залежностей спрямовані в протилежні сторони. Потік керування йде зверху вниз: UI → ViewModel → UseCase → Repository. Потік залежностей йде знизу вгору: Repository реалізує інтерфейс, визначений у UseCase. Repository (нижній рівень) залежить від UseCase (верхній рівень).

Ця інверсія — ключова відмінність DIP від звичайного розділення на шари. У традиційній шаруватій архітектурі кожен шар залежить від шару нижче. В архітектурі з DIP усі шари залежать від абстракцій, а реалізація цих абстракцій знаходиться в шарі інфраструктури, який «підключається» до верхніх шарів через механізми DI.

Як працює принцип інверсії залежностей

Механізм DIP реалізується через визначення абстракцій у модулях верхнього рівня та їх реалізацію в модулях нижнього рівня. Модуль верхнього рівня оголошує інтерфейс для необхідної йому функціональності. Модуль нижнього рівня реалізує цей інтерфейс. Збірка (wiring) відбувається на рівні композиційного кореня додатка.

Процес впровадження DIP в існуючий код: виділити інтерфейс для низькорівневого модуля, перенести цей інтерфейс у модуль верхнього рівня (або в окремий шар абстракцій), переписати залежність верхнього модуля на інтерфейс, змусити низькорівневий модуль реалізувати цей інтерфейс. Після цих кроків напрям залежності змінився на протилежний.

DIP вимагає наявності механізму композиційного кореня — точки в додатку, де створюються всі залежності та зв'язуються одна з одною. В Android це Application.get() або Hilt-компонент, в iOS — AppDelegate або SceneDelegate. Композиційний корінь — єдине місце, де код знає про конкретні реалізації.

Ізоляція шарів через DIP

DIP формує архітектурні межі між шарами додатка. Коли ViewModel залежить від інтерфейсу UserRepository, між presentation та domain-шаром виникає межа: ViewModel (presentation) не знає, звідки беруться дані. Ця межа дозволяє змінювати реалізацію UserRepository (Room → REST → Mock) без впливу на ViewModel. Чим більше таких меж, тим стійкішим є додаток до змін фреймворків і бібліотек.

В Android-архітектурі, рекомендованій Google, DIP реалізується через UseCase, які знаходяться в domain-шарі та залежать від інтерфейсів Repository. RepositoryImpl знаходяться в data-шарі та реалізують ці інтерфейси. Presentation-шар (ViewModel) залежить від UseCase. Напрям залежностей іде від presentation до domain, від domain до data — але жоден шар не знає про конкретні реалізації іншого шару.

Відмінність DIP від DI (Dependency Injection)

DIP і DI часто плутають, але це різні концепції. DIP — архітектурний принцип (ЩО потрібно робити: залежати від абстракцій). DI — паттерн реалізації (ЯК це зробити: передавати залежності через конструктор). DIP відповідає на питання «на чому мають базуватися модулі?», DI — «як об'єкти отримують свої залежності?».

Dependency Injection — спосіб впровадження залежностей в об'єкт через конструктор, метод або властивість. Коли в Kotlin-клас передається інтерфейс Repository через конструктор — це DI. А те, що клас ViewModel залежить від інтерфейсу Repository, а не від конкретної реалізації RoomRepository — це DIP. DI — інструмент, DIP — мета.

Можна дотримуватися DIP без DI-фреймворку: ручне зв'язування залежностей у композиційному корені — це теж DI (manual DI). Можна використовувати DI-фреймворк (Dagger, Hilt, Koin), порушуючи DIP: якщо ViewModel безпосередньо створює об'єкт Repository через new() — DIP порушено, навіть якщо фреймворк встановлено. DIP — архітектурне рішення, DI — технічна деталь.

Приклади DIP у мобільній розробці

Розглянемо Android-приклад застосування DIP до шару даних. Без DIP ViewModel безпосередньо створює RoomDatabase та DAO. З DIP — ViewModel залежить від інтерфейсу UserRepository, а конкретна реалізація RoomUserRepository підставляється ззовні.

kotlin
// Абстракція належить domain-шару (верхній рівень)
interface UserRepository {
    fun getUser(id: Int): User
}

// Domain-шар залежить тільки від абстракції
class GetUserUseCase(
    private val repo: UserRepository
) {
    fun execute(id: Int): User = repo.getUser(id)
}

// Реалізація в data-шарі залежить від абстракції domain-шару
class RoomUserRepository(
    private val dao: UserDao
) : UserRepository {
    override fun getUser(id: Int): User {
        return dao.getById(id)
    }
}

// Композиційний корінь
class AppModule {
    fun provideUserRepository(dao: UserDao): UserRepository {
        return RoomUserRepository(dao)
    }
}

iOS-приклад з Application Coordinator та протоколом для навігації:

swift
// Абстракція навігації в domain-шарі
protocol AuthNavigation {
    func navigateToHome()
    func navigateToLogin()
}

// ViewModel залежить від абстракції, не від UIKit
final class AuthViewModel {
    private let navigation: AuthNavigation

    init(navigation: AuthNavigation) {
        self.navigation = navigation
    }

    func onLoginSuccess() {
        navigation.navigateToHome()
    }
}

// Coordinator (UIKit-шар) реалізує протокол domain-шару
final class AppCoordinator: AuthNavigation {
    func navigateToHome() {
        // UIKit-код навігації
    }
    func navigateToLogin() {
        // UIKit-код навігації
    }
}

Ключовий момент: AuthViewModel (domain) не знає про існування AppCoordinator (UIKit). Він знає лише протокол AuthNavigation. Якщо завтра UIKit заміниться на SwiftUI — AuthViewModel не потребує змін. DIP робить domain-шар незалежним від фреймворків і бібліотек UI.

Інструменти для DIP: Dagger, Hilt, Koin

Hilt — стандартний інструмент DI для Android, рекомендований Google. Вбудований в Jetpack, підтримує ViewModel, Fragment, Service та інші компоненти Android. Hilt автоматизує створення композиційного кореня через анотації @Module, @Provides, @Inject. Використання Hilt не гарантує дотримання DIP — інтерфейс UserRepository має бути визначений у domain-шарі, а не в data-шарі.

Koin — легкий DI-фреймворк для Kotlin без генерації коду та процесора анотацій. DSL Koin (module, single, factory) простіший для вивчення, але перевірка залежностей відбувається в рантаймі, а не на етапі компіляції. Koin популярний у мультиплатформних проектах (KMP) завдяки підтримці iOS.

Dagger 2 — попередник Hilt, досі використовується у великих проектах. Dagger генерує DI-код на етапі компіляції, що дає максимальну продуктивність і діагностику помилок на етапі збірки. Hilt побудований поверх Dagger і надає спрощений API. Для нових проектів Google рекомендує Hilt як основний DI-фреймворк.

Організація модулів DI за шарами

DI-модулі повинні відповідати архітектурним шарам і розділятися на DomainModule, DataModule, PresentationModule. DomainModule надає лише абстракції та UseCase. DataModule надає реалізації для абстракцій. PresentationModule зв'язує ViewModel з UseCase. Така організація гарантує, що domain-шар залишається незалежним від інфраструктурних бібліотек.

При міграції між DI-фреймворками (наприклад, з Koin на Hilt) структура DomainModule не змінюється — змінюються лише способи зв'язування в DataModule та PresentationModule. DIP забезпечує ізоляцію domain-логіки, а DI-фреймворк — технічний механізм зв'язування.

Часто задавані питання

Чи завжди потрібно застосовувати DIP?

DIP необхідний на архітектурних межах — між шарами додатка (domain → data, presentation → domain). Всередині одного шару DIP може бути надлишковим. Наприклад, утилітарний клас StringFormatter всередині domain-шару не потребує інтерфейсу — якщо немає передумов для його заміни.

Чи є DIP тим самим, що й впровадження залежностей?

Ні. DIP — принцип: модулі повинні залежати від абстракцій. DI — паттерн: об'єкт отримує залежності ззовні, а не створює їх сам. DI — спосіб реалізації DIP, але DIP можна дотримуватися і без DI (через фабрики або сервіс-локатор). DI без DIP можливий, але не має архітектурної цінності.

Де визначати інтерфейси для DIP?

Інтерфейси належать тому модулю, який їх використовує, а не тому, який їх реалізує. UserRepository оголошується в domain-шарі, а реалізується в data-шарі. Це ключове правило DIP: власник абстракції — споживач, а не постачальник реалізації.

Як DIP впливає на тестування?

DIP робить тестування можливим на ізольованих шарах. ViewModel, яка залежить від UserRepository (інтерфейс), тестується з mock-реалізацією без бази даних. Без DIP ViewModel залежала б від RoomUserRepository і потребувала налаштування БД для кожного тесту. DIP + DI дають повну ізоляцію модулів при тестуванні.

Який DI-фреймворк вибрати для Android?

Hilt — стандартний вибір для Android-проектів, рекомендований Google. Koin — альтернатива для проектів Kotlin Multiplatform. Dagger 2 — для існуючих проектів, де міграція на Hilt невиправдана. Вибір фреймворку не скасовує необхідності дотримуватися DIP на рівні архітектури.

Підсумки

  • DIP (Dependency Inversion Principle) — п'ятий принцип SOLID про архітектурні межі через абстракції
  • Модулі верхнього рівня не залежать від модулів нижнього — обидва залежать від абстракцій
  • DIP ≠ DI: принцип проти паттерну реалізації; DI — спосіб, DIP — мета
  • Абстракції належать споживачу (domain-шару), а не постачальнику (data-шару)
  • Композиційний корінь — єдине місце в додатку, де збираються конкретні залежності
  • Hilt, Koin, Dagger — інструменти DI, що автоматизують зв'язування, але не замінюють архітектурне рішення DIP
  • Domain-шар, побудований за DIP, залишається незалежним від фреймворків, UI та інфраструктурних бібліотек

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також