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

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