Внедрение зависимостей через поля — плохая практика

в 12:13, , рубрики: dependency injection, java, spring, spring framework, spring ioc, перевод

Перевод статьи Field Dependency Injection Considered Harmful за авторством Vojtech Ruzicka

image

Внедрение зависимостей через поля является очень популярной практикой в DI-фреймворках, таких как Spring. Тем не менее, у этого метода есть несколько серьезных компромиссов и поэтому стоит чаще избегать его.

Типы внедрений

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

Конструктор

private DependencyA dependencyA;
private DependencyB dependencyB;
private DependencyC dependencyC;

@Autowired
public DI(DependencyA dependencyA, DependencyB dependencyB, DependencyC dependencyC) {
    this.dependencyA = dependencyA;
    this.dependencyB = dependencyB;
    this.dependencyC = dependencyC;
}

Сеттер

private DependencyA dependencyA;
private DependencyB dependencyB;
private DependencyC dependencyC;

@Autowired
public void setDependencyA(DependencyA dependencyA) {
    this.dependencyA = dependencyA;
}

@Autowired
public void setDependencyB(DependencyB dependencyB) {
    this.dependencyB = dependencyB;
}

@Autowired
public void setDependencyC(DependencyC dependencyC) {
    this.dependencyC = dependencyC;
}

Поле

@Autowired
private DependencyA dependencyA;
@Autowired
private DependencyB dependencyB;
@Autowired
private DependencyC dependencyC;

Что не так?

Как можно наблюдать, вариант внедрения через поле выглядит очень привлекательным. Он очень лаконичен, выразителен, отсутствует шаблонный код. По коду легко перемещаться и читать его. Ваш класс может просто сфокусироваться на основной функциональности и не загромождается шаблонным DI-кодом. Вы просто помещаете аннотацию @Autowired над полем — и все. Не надо писать специальных конструкторов или сеттеров только для того, чтобы DI-контейнер предоставил необходимые зависимости. Java довольно многословна сама по себе, так что стоит использовать любую возможность, чтобы сделать код короче, верно?

Нарушение принципа единственной ответственности

Добавлять новые зависимости просто. Возможно даже слишком просто. Нет никакой проблемы добавить шесть, десять или даже более зависимостей. При использовании конструкторов для внедрения, после определенного момента число аргументов конструктора становится слишком большим и тут же становится очевидно, что что-то не так. Наличие слишком большого количества зависимостей обычно означает, что у класса слишком много зон ответственности. Это может быть нарушением принципов единственной ответственности (single responsibility) и разделения ответственности (ориг.: separation of concerns) и является хорошим индикатором, что класс возможно стоит более внимательно изучить и подвергнуть рефакторингу. При использовании внедрения через поля такого явного тревожного индикатора нет, и таким образом происходит неограниченное разрастание внедренных зависимостей.

Сокрытие зависимостей

Использование DI-контейнера означает, что класс более не ответственен за управление его зависимостями. Ответственность за их получение выносится из класса во вне и теперь кто-то другой ответственен за их предоставление: это может быть DI-контейнер или ручное предоставление их через тесты. Когда класс более не отвечает за получение зависимостей, он должен явно взаимодействовать с ними, используя публичные интерфейсы — методы или конструкторы. Таким образом становится четко понятно, что требует класс, а также опциональные ли это зависомости (через сеттеры) или обязательные (конструктор)

Зависимость от DI-контейнера

Одна из ключевых идей DI-фреймворков заключается в том, что управляемый класс не должен зависеть от конкретного используемого контейнера. Другими словами, это должен быть простой POJO-класс, экземпляр которого может быть создан самостоятельно, если вы передадите ему все необходимые зависимости. Таким образом, вы можете создать его в юнит-тесте без запуска контейнера и протестировать его отдельно (с контейнером это будет скорее интеграционный тест). Если нет завязки на контейнер, вы можете использовать класс как управляемый или неуправляемый, или даже переключиться на другой DI-фреймворк.
Однако при внедрении прямо в поля вы не предоставляете прямого способа создания экземпляра класса со всеми необходимыми зависимостями. Это означает, что:

  • Существует способ (путем вызова конструктора по-умолчанию) создать объект с использованием new в состоянии, когда ему не хватает некоторых из его обязательных зависимостей, и использование приведет к NullPointerException
  • Такой класс не может быть использован вне DI-контейнеров (тесты, другие модули) и нет способа кроме рефлексии предоставить ему необходимые зависимости

Неизменность

В отличие от способа с использованием конструктора, внедрение через поля не может использоваться для присвоения зависимостей final-полям, что приводит к тому, что ваши объекты становятся изменяемыми

Внедрение через конструктор vs сеттер

Таким образом, инъекция через поля может не быть хорошим способом. Что остается? Сеттеры и конструкторы. Какой из них следует использовать?

Сеттеры

Сеттеры следует использовать для инъекций опциональных зависимостей. Класс должен быть способен функционировать, даже если они не были предоставлены. Зависимости могут быть изменены в любое время после создания объекта. Это может быть, а может и не быть преимуществом в зависимости от обстоятельств. Иногда предпочтительно иметь неизменяемый объект. Иногда же полезно менять составные части объекта во время выполнения — например управляемые бины MBean в JMX.
Официальная рекомендация из документации по Spring 3.x поощряет использование сеттеров над конструкторами:

Команда Spring главным образом выступает за инъекцию через сеттеры, потому что большое количество аргументов конструктора может стать громоздким, особенно если свойства являются необязательными. Сеттеры также делают объекты этого класса пригодными для реконфигурации или повторной инъекции позже. Управление через JMX MBeans является ярким примером

Некоторые пуристы предпочитают инъекцию на основе конструктора. Предоставление всех зависимостей означает, что объект всегда возвращается в вызывающий код в полностью инициализированном состоянии. Недостатком является то, что объект становится менее поддающимся реконфигурации и повторной инъекции

Конструкторы

Инъекция через конструкторы хороша для обязательных зависимостей — тех, которые требуются для корректной функциональности объекта. Передавая их через конструктор, вы можете быть уверенными в том, что объект полностью готов к использованию с момента создания. Поля, присвоенные в конструкторе, также могут быть final, что позволяет объекту быть полностью неизменным или как минимум защищает необходимые поля.

Одно из следствий использования внедрения через конструктор — это то что теперь невозможна циклическая зависимость между двумя объектами, созданными таким образом (в отличие от внедрения через сеттер). Это скорее плюс, чем ограничение, поскольку следует избегать циклических зависимостей, что обычно является признаком плохой архитектуры. Таким образом предотвращается подобная практика.

Еще одним преимуществом является то, что при использовании Spring версий 4.3+ вы можете полностью отвязать ваш класс от конкретного DI-фреймворка. Причина в том, что Spring теперь поддерживает неявное внедрение через конструктор для сценариев использования с одним конструктором. Это означает, что вам больше не нужны DI-аннотации в вашем классе. Конечно, вы можете достигнуть того же результата с помощью явного конфигурирования DI в настройках Spring для данного класса; просто сейчас это сделать гораздо проще.

Что касается Spring 4.x, официальная рекомендация из документации изменилась и теперь инъекция через сеттер более не предпочтительна над конструктором:

Команда Spring главным образом выступает за инъекцию через конструктор, поскольку она позволяет реализовывать компоненты приложения как неизменяемые объекты и гарантировать, что требуемые зависимости не null. Более того, компоненты, внедренные через через конструктор, всегда возвращаются в клиентский код в полностью инициализированном состоянии. Как небольшое замечание, большое число аргументов конструктора является признаком «кода с запашком» и подразумевает, что у класса, вероятно, слишком много обязанностей, и его необходимо реорганизовать, чтобы лучше решать вопрос о разделении ответственности.

Инъекция через сеттер должна использоваться в первую очередь для опциональных зависимостей, которым могут быть присвоены значения по-умолчанию внутри класса. В противном случае, проверки на not-null должны быть использованы везде, где код использует эти зависимости. Одно из преимуществ использования внедрения через сеттеры заключается в том, что они делают объекты класса поддающимися реконфигурации и повторному инжектированию позже

Заключение

В основном стоит избегать внедрения через поля. Как альтернативу для внедрения следует использовать сеттеры или конструкторы. У каждого из них есть свои преимущества и недостатки в зависимости от ситуации. Однако так как эти подходы можно смешивать, это не выбор «или-или» и вы можете в одном классе комбинировать инъекцию и через сеттер, и через конструктор. Конструкторы больше подходят для обязательных зависимостей и при нужде в неизменяемых объектах. Сеттеры лучше подходят для опциональных зависимостей.

Автор: gmananton

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js