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("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, зачем он нужен и как его применять, мы готовы перейти к изучению различных способов внедрения зависимостей, чтобы выбрать подходящий инструмент для каждой ситуации.

Комментарии (3)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Никита Уровень 104
25 октября 2025
До этого модуля, что никто не дожил?
Александр Ф Уровень 112
11 ноября 2025
Ряды редеют(
Ираклий Уровень 112
18 декабря 2025
как раз гдет осенью обновили на джавараше раздел со спрингом