Назад к основам: Внедрение зависимости (DI)

После стольких лет почти повсеместного применения паттерна Dependency Injection (DI) мы наблюдаем все больше и больше сообщений и обсуждений, оспаривающих его ценность. Некоторые авторы даже доходят до того, что возражают против его использования. Однако, большинство критических высказываний против DI состоят из заблуждений и откровенной лжи.
В этой статье мы бы хотели поговорить о ценности этого паттерна, вернувшись к истокам.
Объяснение для пятилетних
Представьте себе очень простую зависимость между двумя классами: класс «Автомобиль» (Car) зависит от класса «Двигатель» (CarEngine).
Однако, мы знаем, что это стоит программировать с помощью интерфейса.
Получится примерно такой код:

public interface Engine {
    boolean isStart();
}

class CarEngine implements Engine {

    @Override
    public boolean isStart() {
        return true;
    }
}

class Car {

    public void start() {
        Engine engine = new CarEngine();
        if (engine.isStart()) {
            System.out.println("Start!");
        }
    }
}
Однако, в итоге, данный код будет соответствовать несколько иной диаграмме классов:
Чтобы изолировать класс Car, недостаточно ввести интерфейс Engine. В коде класса Car также должно быть невозможным создание нового экземпляра класса CarEngine:

class Car {

    private Engine engine;
 
    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        if (engine.isStart()) {
            System.out.println("Start!");
        }
    }
}
Теперь с помощью этого дизайна можно создавать экземпляры класса Car:

Car car = new Car(new CarEngine());
Концепция Dependency Injection состоит в том, чтобы перенести ответственность за создание экземпляра объекта из тела метода за пределы класса и передать уже созданный экземпляр объекта обратно. Вот и все!
Думаю, вряд ли есть какие-либо аргументы против самого принципа DI. Тогда откуда этот критический тренд? Наше предположение заключается в том, что это часто возникает из-за недостаточной осведомленности о различных аспектах DI.
Типология
DI фреймворки могут быть сгруппированы по различным признакам.
Вот некоторые из них.
Внедрение зависимостей (инъекция) во время выполнения кода и во время его компиляции
Поскольку инъекция во время выполнения увеличивает время, необходимое для запуска приложения, оно подходит не для всех типов приложений. Она, например, не подходит для тех приложений, которые запускаются много раз, и работают в течение короткого периода времени. В этом случае более актуальным является внедрение зависимостей во время компиляции. Так, например, обстоит дело с Android-приложениями.
Внедрения зависимостей с помощью конструктора, сеттера и поля класса
Пример с классом Car выше описывал внедрение зависимости через конструктор класса.
Однако, это не единственный способ внедрения зависимостей.
Альтернативы включают в себя
—внедрение зависимости через сеттер.

Car car = new Car();
car.setEngine(new CarEngine());
Этот подход не является хорошей идеей, так как нет причин, по которым зависимость должна меняться во время жизненного цикла внедряемого объекта.
— внедрение зависимости через поле класса.

class Car {
    private Engine engine = new CarEngine();
    ...
}
Этот способ еще хуже, потому что он требует не только рефлексии, но и обхода проверок безопасности (если они имеются, см. Security manager в Java).
Несмотря на то, что некоторые DI-фреймворки, а также некоторые фреймворки для тестирования допускают описанные выше способы внедрения, их следует избегать любой ценой.
Явное и неявное связывание
Некоторые фреймворки допускают неявную инъекцию зависимостей, также называемую autowiring. Чтобы выполнить инъекцию, такие фреймворки будут искать в контексте подходящего кандидата. И потерпят неудачу, если не найдут ни одного подходящего класса или более одного.
Другие фреймворки допускают явное внедрение зависимостей: в этом случае разработчику необходимо сконфигурировать инъекцию путем явной привязки отношения между объектом и зависимостью.
Способы конфигурации
Каждый фреймворк предоставляет один или несколько способов своей конфигурации.
Но давайте сначала поговорим о слоне в комнате. Фреймворк Spring используется настолько повсеместно, что иногда под ним подразумевают сам шаблон DI. Это абсолютно не так! Как было показано в предыдущем разделе, применение DI не требует каких-либо фреймворков. И есть гораздо больше DI фреймворков, чем просто Spring, даже если последний имеет огромную популярность.
Фреймворк Spring позволяет использовать много различных способов конфигурации:
  • XML
  • Аннотации
  • Классы конфигурации Java
  • Groovy скрипты
  • Kotlin, через Bean definition DSL
Хотя DI не может быть ограничен рамками одного Spring, последний также не может быть сведен к первому! Spring основывается на DI, но также предлагает большой набор дополнительной функциональности.
Резюме
Краткое описание фреймворков и их особенностей в соответствии с вышеприведенными критериями:
Spring framework
  • Де факто стандарт для серверных Java приложений
  • Инъекция во время выполнения
  • Внедрение зависимости через конструктор, сеттер и поле
  • Описанные выше способы конфигурации
  • Явное и неявное (autowiring) связывание
Context and Dependency Injection
  • Часть спецификации Java EE
  • Инъекция во время выполнения
  • Внедрение зависимости через конструктор, сеттер и поле
  • Конфигурация только при помощи аннотаций
  • Явное и неявное связывание c акцентом на последнее
Google Guice
  • Инъекция во время выполнения
  • Внедрение зависимости через конструктор, сеттер и поле
  • Конфигурация только при помощи аннотаций
  • Неявное связывание (Autowiring)
PicoContainer
  • «Легкий» DI фреймворк, но мы не имели с ним дела ранее
Dagger 2
  • Де-факто стандарт для Android
  • Инъекция во время компиляции
  • Внедрение зависимости через конструктор, поле или метод с акцентом на первые два способа
  • Конфигурация при помощи аннотаций и внешних классов
  • Неявное связывание
Заключение
Итак, зачем вы используете DI?
На самом деле вопрос должен быть другим — почему вы не используете DI?
Это сделает ваш код более модульным, что будет способствовать более простому обновлению кода, а также лучшему тестированию.
В этой статье я попытался описать набор доступных опций.
— Можно запрограммировать инъекцию или настроить инфраструктуру для этого.
— Можно выбрать среду выполнения или временную структуру DI для компиляции.
— Можно выбрать легкий контейнер, или полноценный, с набором доп. функций.
В последнем случае одна из них наверняка имеет отношение к вашему контексту.
Оригинал статьи «Back to basics: Dependency Injection»
Оцените статью, если она вам понравилась!