JavaRush /Курсы /Модуль 5. Spring /Различные способы внедрения зависимостей: через конструкт...

Различные способы внедрения зависимостей: через конструкторы, сеттеры, поля

Модуль 5. Spring
2 уровень , 2 лекция
Открыта

Теперь, когда мы знаем, зачем нам нужен DI и как Spring управляет зависимостями, пришло время углубиться в различные способы внедрения зависимостей. Мы уже немного об этом говорили на прошлом уроке.

Внедрение зависимостей — это процесс передачи объекта (или его ссылки) другому объекту, которому он нужен, вместо того, чтобы объект сам создавал свои зависимости. Spring предоставляет три основных способа внедрения зависимостей:

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

Каждый из этих подходов имеет свои плюсы и минусы, а выбор конкретного способа зависит от контекста задачи. Давайте внимательно изучим каждый из них и рассмотрим, где и как их лучше применять.


Внедрение зависимостей через конструкторы

Преимущества:

  • Иммутабельность: зависимость задают при создании объекта, и её нельзя изменить после инстанциации.
  • Обязательность зависимости: если конструктор требует параметры, Spring обязательно передаст их, иначе будет ошибка.

Недостатки:

  • Много параметров в конструкторе могут сделать код менее читабельным.
  • При большом количестве зависимостей конструкторы становятся громоздкими.

Пример:


import org.springframework.stereotype.Component;

// Зависимость, которую мы будем внедрять
@Component
public class Engine {
    public void start() {
        System.out.println("Engine is starting...");
    }
}

// Основной класс
@Component
public class Car {
    private final Engine engine;

    // Внедрение зависимости через конструктор
    public Car(Engine engine) {
        this.engine = engine;
    }

    public void drive() {
        engine.start();
        System.out.println("Car is driving...");
    }
}

Как работает:

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("Transmission engaged.");
    }
}

// Основной класс
@Component
public class Truck {
    private Transmission transmission;

    // Внедрение зависимости через сеттер
    @Autowired
    public void setTransmission(Transmission transmission) {
        this.transmission = transmission;
    }

    public void haul() {
        transmission.engage();
        System.out.println("Truck is hauling cargo...");
    }
}

Как работает: 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("Brakes applied!");
    }
}

// Основной класс
@Component
public class Bicycle {
    @Autowired
    private BrakeSystem brakeSystem; // Внедрение зависимости через поле

    public void stop() {
        brakeSystem.applyBrakes();
        System.out.println("Bicycle has stopped.");
    }
}

Как работает: 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("Processing data...");
    }
}

@Component
class Memory {
    public void load() {
        System.out.println("Memory loaded!");
    }
}

@Component
class HardDrive {
    public void save() {
        System.out.println("Data saved to hard drive.");
    }
}

// Основной класс
@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("Computer is running!");
    }
}

С помощью этого примера мы видим, как можно одновременно использовать разные способы DI.


Когда использовать каждый подход?

  1. Конструкторный DI:

    • Зависимость обязательна для работы класса.
    • Объект immutable.
    • Вам важно, чтобы при создании объекта зависимость обязательно была передана.
  2. Сеттерный DI:

    • Зависимость опциональна.
    • Требуется гибкость в изменении зависимости после инстанциации.
  3. DI через поля:

    • Если важна лаконичность кода.
    • В случае, когда тестирование не является первоочередной задачей.

Подводные камни и типичные ошибки

При внедрении зависимостей можно столкнуться с некоторыми распространенными проблемами:

  1. Циклические зависимости. Например, если бин A зависит от бина B, а бин B зависит от бина A, Spring не сможет инициализировать их. Чтобы решить эту проблему, можно воспользоваться аннотацией @Lazy или пересмотреть архитектуру приложения.

  2. Отсутствие зависимости. Если вы забыли указать бин или @Autowired, Spring выбросит NoSuchBeanDefinitionException. Всегда проверяйте настройки конфигурации.

  3. Неоднозначность зависимостей. Если Spring находит несколько бинов одного типа, он не сможет их автоматом внедрить. Используйте аннотацию @Qualifier, чтобы явно указать нужный бин.


@Autowired
@Qualifier("specificBeanName")
private MyService myService;

Теперь вы знаете, как внедрять зависимости в Spring, и уверенно сможете выбирать подходящий способ для каждого конкретного случая. В следующей лекции мы рассмотрим, как управлять жизненным циклом бинов и добавлять дополнительные настройки!

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ