JavaRush /Курси /Модуль 5. Spring /Основні принципи Dependency Injection (DI)

Основні принципи Dependency Injection (DI)

Модуль 5. Spring
Рівень 2 , Лекція 1
Відкрита

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

Уявіть ресторан. Готувати страви — відповідальність шефа. Звісно, він не буде бігати по ринку вишукувати найкращі овочі і тим паче не буде вирощувати корів для стейку. Цим займаються інші фахівці, а ресторан постачає кухні всі потрібні інгредієнти. У програмуванні все влаштовано схоже: об'єкт не повинен сам створювати свої залежності — це має робитись зовні.

Впровадження залежностей (DI) робить код:

  1. Гнучким: якщо треба замінити залежність (наприклад, перейти з однієї бази даних на іншу), це можна зробити без переписування всієї логіки.
  2. Зручним для тестування: ви легко можете підмінити залежності заглушками (mock) для написання тестів.
  3. Легшим у підтримці: зміни в одній залежності мінімально зачіпають інший код.

Проблема традиційного підходу

Розглянемо приклад без DI:


public class OrderService {
    private final PaymentService paymentService;

    public OrderService() {
        this.paymentService = new PaymentService(); // Самі створюємо екземпляр залежності
    }

    public void placeOrder() {
        paymentService.processPayment();
    }
}

Що тут не так? Ну, як мінімум:

  1. OrderService залежить від конкретної реалізації PaymentService. Якщо треба використати інший PaymentService — доведеться міняти код.
  2. Тестувати 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:

  1. Через конструктор
  2. Через сеттер
  3. Через поле

Ми розглянемо їх детальніше в наступній лекції, але нижче наведемо короткий огляд.

Впровадження через конструктор

У цьому підході залежності передаються в параметрах конструктора:


@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 корисний мінімум у всіх веб‑додатках, в мікросервісних проектах і при тестуванні.

  1. Мікросервіси: в мікросервісах DI використовується для ін'єкції сервісів, репозиторіїв, конфігурацій і клієнтів API.
  2. Веб-додатки: DI дозволяє зберігати лише один екземпляр сервісу, який використовують всі контролери для обробки запитів.
  3. Тестування: 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, навіщо це потрібно і як це застосовувати, можемо переходити до вивчення різних способів впровадження залежностей, щоб вибрати підходящий інструмент для кожної ситуації.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ