📐 Розділ 18 · Питання #8

Що таке принцип Dependency Inversion?

Уявіть електричну розетку на стіні та побутову техніку:


🟢 Junior Level

Просте визначення

Принцип інверсії залежностей (Dependency Inversion Principle, DIP) — це остання літера «D» в абревіатурі SOLID. Він стверджує:

  1. Модулі вищого рівня не повинні залежати від модулів нижчого рівня. Обидва повинні залежати від абстракцій.
  2. Абстракції не повинні залежати від деталей. Деталі повинні залежати від абстракцій.

Простими словами:

  • Абстракція — це інтерфейс або контракт (ЩО потрібно зробити, наприклад PaymentProcessor або EmailSender).
  • Деталь — це конкретна реалізація (ЯК це зроблено, наприклад PostgresUserRepository або TwilioSmsSender).
  • Бізнес-логіка (вищий рівень) повинна залежати від інтерфейсів, а конкретні бази даних та сторонні бібліотеки (нижчий рівень) мають підлаштовуватися під ці інтерфейси.

Аналогія з реального життя

Уявіть електричну розетку на стіні та побутову техніку:

  • Розетка на стіні — це стандартний інтерфейс (220V, євровилка).
  • Пилосос, холодильник і зарядка для смартфона залежать від стандарту розетки, а не припаяні намертво до дротів у стіні.
  • Якщо ви захочете замінити пилосос на новий, вам не потрібно розбивати стіну й міняти проводку — ви просто вставляєте іншу вилку в ту саму розетку.

Приклад порушення та виправлення

❌ Погано (Пряма жорстка залежність від деталей):

// Деталь нижчого рівня
public class MySQLDatabase {
    public void save(String data) {
        System.out.println("Збереження в MySQL: " + data);
    }
}

// Модуль вищого рівня напряму залежить від MySQLDatabase!
public class UserService {
    private MySQLDatabase database = new MySQLDatabase(); // Жорсткий зв'язок!

    public void registerUser(String username) {
        // Якщо захочемо перейти на PostgreSQL або Mongo — доведеться переписувати UserService
        database.save(username);
    }
}

✅ Добре (Залежність через абстракцію):

// Абстракція (контракт)
public interface UserRepository {
    void save(String data);
}

// Деталь 1: PostgreSQL
public class PostgresUserRepository implements UserRepository {
    @Override
    public void save(String data) {
        System.out.println("Збереження в PostgreSQL: " + data);
    }
}

// Деталь 2: InMemory для швидких unit-тестів
public class InMemoryUserRepository implements UserRepository {
    private final List<String> storage = new ArrayList<>();
    @Override
    public void save(String data) { storage.add(data); }
}

// Модуль вищого рівня залежить ТІЛЬКИ від інтерфейсу UserRepository
public class UserService {
    private final UserRepository repository;

    // Впровадження залежності через конструктор (DI)
    public UserService(UserRepository repository) {
        this.repository = repository;
    }

    public void registerUser(String username) {
        repository.save(username);
    }
}

🟡 Middle Level

У чому полягає «Інверсія»?

У класичному процедурному програмуванні потік залежностей повторює потік керування:

[Бізнес-логіка (High-level)] ───► [Доступ до БД (DAO)] ───► [Драйвер PostgreSQL]

Тут високорівневий модуль намертво «прив’язаний» до технологій нижчого рівня.

При інверсії залежностей стрілка розвертається:

[Бізнес-логіка (High-level)] ───► [«Інтерфейс Репозиторію» (Абстракція)]
                                                   ▲
                                                   │ implements
                                  [PostgresUserRepository (Low-level)]

Інтерфейс належить бізнес-шару (пакету сервісу), а шар доступу до даних підпорядковується йому.


DIP проти DI проти IoC

Поняття Що це таке Приклад
DIP (Principle) Архітектурний принцип проектування: залежність від абстракцій OrderService залежить від OrderRepository
DI (Pattern) Патерн проектування: передача залежностей ззовні Передача об’єкта через конструктор: new OrderService(repo)
IoC (Framework Concept) Інверсія керування: фреймворк управляє життєвим циклом Spring IoC Container створює біни та інжектує їх

🔴 Senior Level

Чиста та Гексагональна архітектура (Ports & Adapters)

DIP є фундаментом Clean Architecture (Роберт Мартін) та Hexagonal Architecture (Алістер Кокберн):

┌─────────────────────────────────────────────────────────────┐
│                      DOMAIN / CORE                          │
│   (Бізнес-сутності, Use Cases, Інтерфейси портів)            │
│                              ▲                              │
│                              │ DIP (Зворотні залежності)    │
│                     ADAPTERS / INFRA                        │
│   (Postgres, Kafka, REST Controllers, Redis, AWS S3)        │
└─────────────────────────────────────────────────────────────┘
  • Domain Core не знає ні про Spring, ні про Hibernate, ні про Postgres, ні про Kafka.
  • Зовнішня інфраструктура реалізує інтерфейси портів домену. Заміна реляційної БД на MongoDB не вимагає жодного рядка змін у доменній логіці.

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • DIP: модулі вищого рівня не залежать від модулів нижчого рівня; обидва залежать від абстракцій.
  • Абстракції не повинні залежати від деталей. Деталі залежать від абстракцій.
  • Різниця: DIP — архітектурний принцип; DI — патерн передачі залежностей (через конструктор); IoC — контейнер керування життєвим циклом (Spring).
  • DIP є базою для Clean Architecture та Hexagonal Architecture (Ports and Adapters).

Часті уточнюючі запитання:

  • Чому створення new Service() всередині класу порушує DIP? — Це створює жорстку прив’язку до конкретної реалізації, унеможливлюючи підміну на мок у тестах або зміну БД.
  • Кому повинен належати інтерфейс? — Інтерфейс належить модулю-клієнту (вищому рівню), а не модулю-реалізації.

Червоні прапорці (НЕ говорити):

  • ❌ «DIP і Dependency Injection (DI) — це абсолютно одне й те саме».
  • ❌ «DIP потрібен тільки тоді, коли ми використовуємо Spring Framework» (DIP — це базовий ООП-принцип, що застосовується і без фреймворків).

Пов’язані теми: