Тепер, коли ми знаємо, навіщо нам потрібен DI і як Spring керує залежностями, час заглибитись в різні способи впровадження залежностей. Ми вже трохи про це говорили на попередньому уроці.
Впровадження залежностей — це процес передачі об'єкта (або його посилання) іншому об'єкту, якому він потрібен, замість того, щоб об'єкт сам створював свої залежності. Spring надає три основні способи впровадження залежностей:
- Через конструктори.
- Через сеттери.
- Через поля.
Кожен з цих підходів має свої плюси і мінуси, а вибір конкретного способу залежить від контексту задачі. Давай уважно розберемо кожен з них і подивимось, де і як їх краще застосовувати.
Впровадження залежностей через конструктори
Переваги:
- Імм'ютебельність: залежність задають під час створення об'єкта, і її не можна змінити після інстанціації.
- Обов'язковість залежності: якщо конструктор вимагає параметри, Spring обов'язково передасть їх, інакше буде помилка.
Недоліки:
- Багато параметрів у конструкторі можуть зробити код менш читабельним.
- При великій кількості залежностей конструктори стають громіздкими.
Приклад:
import org.springframework.stereotype.Component;
// Залежність, яку ми будемо впроваджувати
@Component
public class Engine {
public void start() {
System.out.println("Двигун запускається...");
}
}
// Головний клас
@Component
public class Car {
private final Engine engine;
// Впровадження залежності через конструктор
public Car(Engine engine) {
this.engine = engine;
}
public void drive() {
engine.start();
System.out.println("Машина їде...");
}
}
Як це працює:
Spring автоматично знаходить підходящий бін Engine і передає його в конструктор Car. Конструкторний підхід також дуже зручний, якщо об'єкт повинен бути строго залежним від іншого.
Впровадження залежностей через сеттери
Переваги:
- Дозволяє задавати залежність після створення об'єкта.
- Більш гнучкий підхід: можна змінити залежність у runtime (хоча таке потрібно нечасто).
Недоліки:
- Неочевидно, чи є залежність обов'язковою. Наприклад, якщо ти забув викликати метод-сеттер, додаток може несподівано впасти.
- Менш підходить для реалізації незмінюваних (immutable) об'єктів.
Приклад коду:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
// Залежність, яку ми будемо впроваджувати
@Component
public class Transmission {
public void engage() {
System.out.println("Трансмісія задіяна.");
}
}
// Головний клас
@Component
public class Truck {
private Transmission transmission;
// Впровадження залежності через сеттер
@Autowired
public void setTransmission(Transmission transmission) {
this.transmission = transmission;
}
public void haul() {
transmission.engage();
System.out.println("Вантажівка перевозить вантаж...");
}
}
Як це працює: Spring викликає метод setTransmission() після створення об'єкта Truck. @Autowired вказує контейнеру IoC, що потрібно впровадити залежність.
Цей спосіб зручний, якщо залежність може змінюватися або якщо об'єкт потребує складної ініціалізації.
Впровадження залежностей через поля
Переваги:
- Найкомпактніший і лаконічний код.
- Потрібно менше коду для впровадження залежності.
Недоліки:
- Для модульного тестування не дуже зручно, бо залежності складно підставити через конструктор.
- Порушує принцип інкапсуляції, оскільки поля впроваджуються напряму.
Приклад:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
// Залежність, яку ми будемо впроваджувати
@Component
public class BrakeSystem {
public void applyBrakes() {
System.out.println("Гальма застосовано!");
}
}
// Головний клас
@Component
public class Bicycle {
@Autowired
private BrakeSystem brakeSystem; // Впровадження залежності через поле
public void stop() {
brakeSystem.applyBrakes();
System.out.println("Велосипед зупинився.");
}
}
Як це працює: Spring впроваджує BrakeSystem безпосередньо у приватне поле brakeSystem. Ця магія досягається за допомогою анотації @Autowired.
Порівняння способів впровадження залежностей
| Підхід | Переваги | Недоліки |
|---|---|---|
| Конструкторний DI | Імм'ютебельність, обов'язковість залежності | Незручно для об'єктів з великою кількістю залежностей |
| Сеттерний DI | Гнучкість, підходить для зміни залежностей у runtime | Можлива робота без встановленої залежності, не підходить для immutable об'єктів |
| DI через поля | Компактний код, мінімальні зусилля | Порушення інкапсуляції, менш зручно для тестів |
Практичний приклад: все разом!
Створимо невеликий приклад, де використовуються всі три підходи одночасно.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
// Залежності
@Component
class Processor {
public void process() {
System.out.println("Обробка даних...");
}
}
@Component
class Memory {
public void load() {
System.out.println("Пам'ять завантажена!");
}
}
@Component
class HardDrive {
public void save() {
System.out.println("Дані збережено на жорсткому диску.");
}
}
// Головний клас
@Component
class Computer {
private final Processor processor; // Впровадження через конструктор
private Memory memory; // Впровадження через сеттер
@Autowired
private HardDrive hardDrive; // Впровадження через поле
@Autowired
public Computer(Processor processor) {
this.processor = processor;
}
@Autowired
public void setMemory(Memory memory) {
this.memory = memory;
}
public void start() {
processor.process();
memory.load();
hardDrive.save();
System.out.println("Комп'ютер працює!");
}
}
За допомогою цього прикладу бачимо, як можна одночасно використовувати різні способи DI.
Коли використовувати кожен підхід?
Конструкторний DI:
- Залежність обов'язкова для роботи класу.
- Об'єкт immutable.
- Ти хочеш, щоб під час створення об'єкта залежність обов'язково була передана.
Сеттерний DI:
- Залежність опціональна.
- Потрібна гнучкість у зміні залежності після інстанціації.
DI через поля:
- Коли важлива лаконічність коду.
- Якщо тестування не є пріоритетом.
Підводні камені та типові помилки
При впровадженні залежностей можна натрапити на кілька поширених проблем:
Циклічні залежності. Наприклад, якщо бін A залежить від біна B, а бін B залежить від біна A, Spring не зможе їх ініціалізувати. Щоб вирішити цю проблему, можна скористатися анотацією
@Lazyабо переглянути архітектуру додатка.Відсутність залежності. Якщо ти забув вказати бін або
@Autowired, Spring викинеNoSuchBeanDefinitionException. Завжди перевіряй налаштування конфігурації.Неоднозначність залежностей. Якщо Spring знаходить кілька бінів одного типу, він не зможе автоматично їх впровадити. Використовуй анотацію
@Qualifier, щоб явно вказати потрібний бін.
@Autowired
@Qualifier("specificBeanName")
private MyService myService;
Тепер ти знаєш, як впроваджувати залежності в Spring, і впевнено зможеш вибирати підходящий спосіб для кожного конкретного випадку. У наступній лекції розберемо, як керувати життєвим циклом бінів та додавати додаткові налаштування!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ