Dependency Injection (впровадження залежностей) може звучати складно, але це просто модне слово для передачі одного об'єкта (залежності) в інший об'єкт. Простіше кажучи, DI — це спосіб, за допомогою якого об'єкт (назвімо його споживач) отримує всі потрібні залежності для виконання своїх задач.
Уявіть ресторан. Готувати страви — відповідальність шефа. Звісно, він не буде бігати по ринку вишукувати найкращі овочі і тим паче не буде вирощувати корів для стейку. Цим займаються інші фахівці, а ресторан постачає кухні всі потрібні інгредієнти. У програмуванні все влаштовано схоже: об'єкт не повинен сам створювати свої залежності — це має робитись зовні.
Впровадження залежностей (DI) робить код:
- Гнучким: якщо треба замінити залежність (наприклад, перейти з однієї бази даних на іншу), це можна зробити без переписування всієї логіки.
- Зручним для тестування: ви легко можете підмінити залежності заглушками (mock) для написання тестів.
- Легшим у підтримці: зміни в одній залежності мінімально зачіпають інший код.
Проблема традиційного підходу
Розглянемо приклад без DI:
public class OrderService {
private final PaymentService paymentService;
public OrderService() {
this.paymentService = new PaymentService(); // Самі створюємо екземпляр залежності
}
public void placeOrder() {
paymentService.processPayment();
}
}
Що тут не так? Ну, як мінімум:
OrderServiceзалежить від конкретної реалізаціїPaymentService. Якщо треба використати іншийPaymentService— доведеться міняти код.- Тестувати
OrderServiceскладніше, бо ми не можемо підмінитиPaymentServiceна заглушку.
Як DI вирішує проблему?
DI вирішує це, передаючи залежності ззовні:
public class OrderService {
private final PaymentService paymentService;
// Залежність передається ззовні через конструктор
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void placeOrder() {
paymentService.processPayment();
}
}
Тепер ми можемо передати будь-яку реалізацію PaymentService (наприклад, CreditCardPaymentService або PayPalPaymentService), що робить код гнучким.
Переваги використання DI
Тестованість
За допомогою DI ви можете легко підмінити реальні залежності на заглушки (моки) для тестування. Наприклад:
PaymentService mockPaymentService = Mockito.mock(PaymentService.class);
OrderService orderService = new OrderService(mockPaymentService);
Підтримуваність
Коли об'єкти не створюють свої залежності самі, їх простіше замінювати в майбутньому. Наприклад, якщо треба додати нову реалізацію PaymentService, можна зробити це, не змінюючи OrderService.
Масштабованість
DI допомагає масштабувати додатки. Уявіть, замість 10 класів у вас 1000, і всі вони створюють свої залежності вручну — керувати всім цим буде справжній жах. DI з цим впорається легше і позбавить вас рутини управління залежностями.
Способи впровадження залежностей
Spring Framework підтримує три основні способи DI:
- Через конструктор
- Через сеттер
- Через поле
Ми розглянемо їх детальніше в наступній лекції, але нижче наведемо короткий огляд.
Впровадження через конструктор
У цьому підході залежності передаються в параметрах конструктора:
@Component
public class OrderService {
private final PaymentService paymentService;
@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void placeOrder() {
paymentService.processPayment();
}
}
У більшості випадків краще використовувати саме цей спосіб, бо він:
- Робить залежності обов'язковими для ініціалізації (компілятор перевіряє наявність конструктора).
- Дозволяє створювати незмінні залежності (final).
Впровадження через сеттер
Залежності передаються через сеттери. Це робить їх необов'язковими:
@Component
public class OrderService {
private PaymentService paymentService;
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void placeOrder() {
paymentService.processPayment();
}
}
Цей підхід краще підходить для конфігурацій за замовчуванням або ситуацій, коли залежності не завжди потрібні.
Впровадження через поле
Цей спосіб використовує анотацію @Autowired прямо на полі. Хоча це найкоротший спосіб, він менш бажаний, бо порушує інкапсуляцію:
@Component
public class OrderService {
@Autowired
private PaymentService paymentService;
public void placeOrder() {
paymentService.processPayment();
}
}
DI в Spring Framework
Отже, у Spring Framework є інструменти для використання DI. І це круто, бо ми тепер не мусимо керувати залежностями "вручну". Основні принципи DI в Spring:
Автоматичне зв'язування залежностей (Autowired)
Анотація @Autowired використовується для автоматичного впровадження потрібних залежностей. Spring сам знаходить підходящу залежність на основі типу.
Приклад:
@Component
public class OrderService {
private final PaymentService paymentService;
@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
Spring автоматично знайде бин типу PaymentService і передасть його в конструктор.
Java-based конфігурація з анотацією @Bean
Ви також можете визначати залежності вручну через конфігураційні класи:
@Configuration
public class AppConfig {
@Bean
public PaymentService paymentService() {
return new PayPalPaymentService();
}
@Bean
public OrderService orderService(PaymentService paymentService) {
return new OrderService(paymentService);
}
}
DI на реальних проектах
Можемо повторити ще раз, але взагалі ви й так знаєте: DI корисний мінімум у всіх веб‑додатках, в мікросервісних проектах і при тестуванні.
- Мікросервіси: в мікросервісах DI використовується для ін'єкції сервісів, репозиторіїв, конфігурацій і клієнтів API.
- Веб-додатки: DI дозволяє зберігати лише один екземпляр сервісу, який використовують всі контролери для обробки запитів.
- Тестування: DI спрощує підміну реальних залежностей моками при написанні тестів.
Реальний приклад
Якщо ви розробляєте REST API для обробки замовлень, DI дозволяє легко зв'язувати контролери, сервіси і репозиторії:
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
@Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<String> createOrder() {
orderService.placeOrder();
return ResponseEntity.ok("Замовлення успішно оформлено!");
}
}
Тут OrderController повністю "обезособлений" — йому не важливо, як влаштований OrderService — він просто його використовує.
Основні помилки і як їх виправити
Циклічні залежності
Якщо два біна посилаються один на одного, Spring не знатиме, як їх впровадити, і викине помилку. Наприклад:
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
Щоб вирішити це, краще переглянути архітектуру і зробити залежності односторонніми.
Неоднозначність залежностей
Якщо у вас кілька бінів одного типу, Spring не знає, який інжектити:
@Component
public class CreditCardPaymentService implements PaymentService {}
@Component
public class PayPalPaymentService implements PaymentService {}
@Component
public class OrderService {
@Autowired
private PaymentService paymentService; // Помилка: який бин обрати?
}
Рішення — використовувати анотацію @Qualifier:
@Component
public class OrderService {
@Autowired
@Qualifier("creditCardPaymentService")
private PaymentService paymentService;
}
От і все: тепер, коли ви розумієте, що таке DI, навіщо це потрібно і як це застосовувати, можемо переходити до вивчення різних способів впровадження залежностей, щоб вибрати підходящий інструмент для кожної ситуації.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ