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("Order placed successfully!");
}
}
Здесь 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, зачем он нужен и как его применять, мы готовы перейти к изучению различных способов внедрения зависимостей, чтобы выбрать подходящий инструмент для каждой ситуации.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ