🍃 Розділ 5 · Питання #1

Що таке Dependency Injection

DI є конкретною практичною реалізацією більш загального архітектурного принципу — Inversion of Control (IoC, Інверсія управління).


🟢 Junior Level

1. Просте визначення концепції

Dependency Injection (DI, Впровадження залежностей) — це патерн проєктування, за якого об’єкт отримує необхідні йому зовнішні ресурси та сервіси (залежності) ззовні, замість того щоб створювати їх самостійно за допомогою оператора new.

DI є конкретною практичною реалізацією більш загального архітектурного принципу — Inversion of Control (IoC, Інверсія управління).

Традиційний підхід (Без DI):
[ OrderService ] ──(створює через new)──► [ OrderRepository ]
OrderService жорстко прив'язаний до конкретного класу OrderRepository.

Підхід Dependency Injection (з DI):
[ IoC Container (Spring) ] ──(створює та впроваджує)──► [ OrderRepository ]
                                                                  │
                                                                  ▼
                                                         [ OrderService ]
OrderService залежить тільки від абстракції та отримує екземпляр ззовні.

2. Порівняння коду: без DI та з DI

Без DI (Жорстка зв’язаність):

public class OrderService {
    // ❌ КЛАС САМ КЕРУЄ ЖИТТЄВИМ ЦИКЛОМ ЗАЛЕЖНОСТІ:
    private final OrderRepository repository = new PostgresOrderRepository();

    public void processOrder(Order order) {
        repository.save(order);
    }
}

Проблеми: неможливо написати ізольований unit-тест (доведеться запускати реальну базу даних Postgres), неможливо підмінити сховище на InMemory або Mongo без зміни вихідного коду сервісу.

З DI (Слабка зв’язаність):

@Service
public class OrderService {
    // ✅ ЗАЛЕЖНІСТЬ НАДАЄТЬСЯ ЗЗОВНІ (ЧЕРЕЗ КОНСТРУКТОР):
    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    public void processOrder(Order order) {
        repository.save(order);
    }
}

Переваги: у тестах можна передати mock-об’єкт new OrderService(mockRepo), клас нічого не знає про спосіб створення залежностей і суворо дотримується принципу єдиної відповідальності (SRP).


🟡 Middle Level

1. Архітектурна тріада: DIP vs IoC vs DI

Поширене джерело плутанини на співбесідах — розмежування трьох споріднених понять:

  1. Dependency Inversion Principle (DIP): принцип літери «D» із SOLID. Це високорівневе архітектурне правило: «Модулі вищих рівнів не повинні залежати від модулів нижчих рівнів. Обидва повинні залежати від абстракцій (інтерфейсів)».
  2. Inversion of Control (IoC): загальний поведінковий патерн («Голлівудський принцип: Не телефонуйте нам, ми самі вам зателефонуємо»). Традиційна програма сама керує потоком виконання; в IoC керування життєвим циклом та виконанням бере на себе фреймворк.
  3. Dependency Injection (DI): прикладний патерн проєктування, що реалізує IoC у частині зв’язування компонентів: контейнер самостійно збирає граф залежностей і встановлює посилання в потрібні поля чи аргументи конструктора.

2. Способи впровадження залежностей у Spring

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

Тип впровадження Синтаксис Незмінність (final) Unit-тестування без Spring Рекомендація
Constructor Injection Через аргументи конструктора ✅ Так (final) ✅ Легко (new Service(mock)) 🟢 Основний стандарт
Setter Injection Через сетери setRepo(...) ❌ Ні ⚠️ Середньо (можна забути викликати) 🟡 Для опціональних залежностей
Field Injection @Autowired private Repo repo; ❌ Ні ❌ Складно (вимагає рефлексії) 🔴 Антипатерн
// Рекомендований сучасний стандарт (Constructor Injection + Lombok):
@Service
@RequiredArgsConstructor
public class PaymentService {
    private final PaymentGateway gateway; // Впроваджується через згенерований конструктор
    private final AuditLogger logger;
}

🔴 Senior Level

1. Життєвий цикл DI в ядрі Spring Framework

Створення та впровадження залежностей усередині Spring ApplicationContext відбувається через чітко розмежовані фази:

1. Сканування класів (@ComponentScan, @Configuration)
   │
2. Формування метаданих (BeanDefinition)
   │
3. Модифікація метаданих (BeanFactoryPostProcessor)
   │
4. Інстанціювання екземпляра (Instantiation via Constructor Resolver)
   │  --> Тут спрацьовує Constructor Injection!
   │
5. Впровадження залежностей (Property Population via AutowiredAnnotationBPP)
   │  --> Тут спрацьовують Setter та Field Injection!
   │
6. Ініціалізація (BeanPostProcessor.postProcessBeforeInitialization, @PostConstruct)
   │
7. Створення Проксі (AOP, @Transactional via postProcessAfterInitialization)
   │
8. Готовий бін у Singleton Pool (Ready to use)

Критичний архітектурний висновок: Constructor Injection виконується на фазі 4 (Instantiation). У цей момент бін фізично не може бути сконструйований без наявності готових екземплярів своїх залежностей у контексті. Field та Setter Injection відбуваються на фазі 5 (Property Population), тобто вже після того, як об’єкт було створено через конструктор без аргументів.


2. Циклічні залежності та рішення Spring Boot 2.6+

Коли Бін $A$ потребує Бін $B$, а Бін $B$ одночасно потребує Бін $A$:

  • При Constructor Injection: Spring не здатний створити $A$ без $B$, а $B$ без $A$. Контейнер аварійно зупиняється на старті застосунку з винятком BeanCurrentlyInCreationException. Це зразок принципу Fail-Fast: архітектурний дефект виявляється миттєво до запуску бізнес-логіки.
  • При Field Injection: У старих версіях Spring маскував цю проблему завдяки створенню сирих незаповнених екземплярів через трирівневий кеш синглтонів (three-level cache у DefaultSingletonBeanRegistry).

[!IMPORTANT] Починаючи зі Spring Boot 2.6, циклічні залежності заборонені за замовчуванням (spring.main.allow-circular-references=false). Спроба запуску контексту з циклічними зв’язками призведе до падіння додатка.

Шляхи вирішення циклів:

  1. Архітектурний рефакторинг (Best Practice): виділення третього спільного сервісу $C$, від якого односпрямовано залежатимуть $A$ і $B$, або перехід на асинхронні події (ApplicationEventPublisher).
  2. Лінива проксі-ініціалізація (Workaround): анотація @Lazy на одному з аргументів конструктора змушує Spring підставити динамічний CGLIB-проксі, відкладаючи реальне зв’язування до першого звернення до методу.

3. DI проти Service Locator

Типовим антипатерном використання Spring є перетворення IoC-контейнера на Service Locator:

// ❌ АНТИПАТЕРН SERVICE LOCATOR:
@Service
public class OrderService {
    @Autowired
    private ApplicationContext context; // Впровадження всього контексту!

    public void process() {
        // Прихована залежність, динамічний пошук за типом:
        PaymentGateway gateway = context.getBean(PaymentGateway.class);
        gateway.pay();
    }
}

У чому небезпека: Клас виявляється жорстко прив’язаним до API Spring (ApplicationContext), залежності стають неявними (їх не видно в сигнатурі конструктора), а повноцінне unit-тестування вимагає ініціалізації важкого контексту Spring.


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

30-секундна відповідь (Elevator Pitch)

«Dependency Injection — це реалізація принципу інверсії управління (IoC), за якої створення та зв’язування залежностей делегуються зовнішньому механізму (IoC-контейнеру Spring), а сам компонент лише декларує свої потреби. Це гарантує слабку зв’язаність (Loose Coupling), дотримання Single Responsibility та просту підміну залежностей тестовими моками. У сучасній розробці єдиним рекомендованим підходом є Constructor Injection, що забезпечує імутабельність полів через final, гарантує повну ініціалізацію об’єкта та виявляє циклічні залежності на етапі запуску додатку (Fail-Fast)».


3 каверзних запитання з інтерв’ю та відповіді

1. У чому різниця між поняттями IoC, DI та DIP?

Відповідь:

  • DIP (Dependency Inversion Principle): високорівневий архітектурний принцип SOLID, який вимагає, щоб класи залежали від абстракцій (інтерфейсів), а не від конкретних реалізацій.
  • IoC (Inversion of Control): загальний поведінковий патерн, що передає контроль над потоком виконання та створенням об’єктів фреймворку.
  • DI (Dependency Injection): конкретний шаблон проєктування, що реалізує IoC, за якого залежності постачаються в об’єкт ззовні (через конструктор або сетер).

2. Чому у Spring Boot 2.6+ циклічні залежності заборонено за замовчуванням?

Відповідь: Циклічна залежність є явною ознакою архітектурної проблеми (Code Smell) та порушення принципу єдиної відповідальності (SRP). Дозвіл циклів маскував помилки дизайну, створював проблеми під час генерації AOP-проксі (проксі не встигали обгорнути сирі екземпляри) і сповільнював старт контексту. Заборона спонукає розробників проєктувати слабозв’язані сервіси або застосовувати шину подій (ApplicationEventPublisher).

3. На якому етапі життєвого циклу біна створюється проксі (@Transactional, @Async), і як це впливає на DI?

Відповідь: Проксі створюється на етапі ініціалізації біна всередині методу BeanPostProcessor.postProcessAfterInitialization(). Це відбувається після того, як у цільовий об’єкт були впроваджені всі залежності (фаза DI завершена). У результаті в інші сервіси впроваджується вже екземпляр згенерованого проксі (CGLIB або JDK Dynamic Proxy), який огортає реальний об’єкт.


Типові помилки та Red Flags

  • ❌ Використання Field Injection (@Autowired на приватних полях): Пряме порушення інкапсуляції, ускладнення unit-тестування, неможливість оголосити поля final.
  • ❌ Звернення до ApplicationContext.getBean() у бізнес-коді: Перетворення DI на Service Locator і паразитна прив’язка до Spring API.
  • ❌ Спроба створювати бінами сутності даних (DTO, Records, JPA Entities): Бінами повинні бути виключно stateless-сервіси, компоненти інфраструктури та конфігурації.
  • ❌ Ігнорування циклічних зв’язків: Використання @Lazy як постійного рішення замість рефакторингу архітектури.

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