Инъекция зависимостей: Инъекция поля в сравнении с Инъекцией в конструктор?

62

Я знаю, что это горячая дискуссия, и мнения, как правило, меняются с течением времени.

Раньше я использовал исключительно полевую инъекцию для своих классов, пока не начал читать в разных блогах (например, petrikainulainen и schauderhaft and fowler ) о преимуществах инъекции в конструктор. С тех пор я переключил свои методологии, чтобы использовать инжектор конструктора для требуемых зависимостей и метод сеттера для необязательных зависимостей.

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

Есть ли в современном мире предпочтительный способ делать инъекции? Предпочтительнее ли вводить в полевых условиях?

За последние пару лет я перешел к инжектору конструктора из полевого ввода, и мне стало намного проще его использовать, но мне интересно, стоит ли мне пересмотреть свою точку зрения. Автор JMockit (Rogério Liesenfeld) , несомненно, хорошо разбирается в DI, поэтому я чувствую себя обязанным пересмотреть мой подход, учитывая, что он так сильно настроен против внедрения конструктора / сеттера.

Эрик Б.
источник
2
Если вам не разрешено использовать внедрение в конструктор или установку , тогда как вы должны передать свои зависимости классу? Может быть, вам лучше связать дискуссию, если ваш разговор с парнем из Мокита не был закрытым.
Роберт Харви
2
@DavidArno: в неизменяемом классе состояние устанавливается один раз ... в конструкторе.
Роберт Харви
1
@Davkd вы описываете анемичную модель предметной области. У него, конечно, есть сторонники, но он больше не ориентирован на объект; где данные и функции, которые действуют на эти данные, живут вместе.
Ричард Тингл
3
@ Дэвид Нет ничего плохого в функциональном программировании. Но java в основном разработан для объектно-ориентированного подхода, и вы будете постоянно бороться с ним и в конечном итоге получите модель анемичной области, если вы не будете очень осторожны. Нет ничего плохого в функционально-ориентированном коде, но для него следует использовать соответствующий инструмент, чего нет в java (даже если в lambda добавлены элементы программирования, основанного на функциях)
Ричард Тингл,

Ответы:

48

Полевая инъекция - это слишком «жуткое действие на расстоянии», на мой вкус.

Рассмотрим пример, который вы предоставили в своем посте групп Google:

public class VeracodeServiceImplTest {


    @Tested(fullyInitialized=true)
    VeracodeServiceImpl veracodeService;
    @Tested(fullyInitialized=true, availableDuringSetup=true)
    VeracodeRepositoryImpl veracodeRepository;
    @Injectable private ResultsAPIWrapper resultsApiWrapper;
    @Injectable private AdminAPIWrapper adminApiWrapper;
    @Injectable private UploadAPIWrapper uploadApiWrapper;
    @Injectable private MitigationAPIWrapper mitigationApiWrapper;

    static { VeracodeRepositoryImpl.class.getName(); }
...
}

Итак, в основном вы говорите: «У меня есть этот класс с закрытым состоянием, к которому я прикрепил @injectableаннотации, что означает, что состояние может автоматически заполняться каким-либо агентом извне, даже если мое состояние было объявлено закрытым. "

Я понимаю мотивы для этого. Это попытка избежать большей части церемонии, которая присуща правильной настройке класса. По сути, кто-то говорит: «Я устал писать все эти шаблоны, поэтому я просто собираюсь аннотировать все свое состояние и позволить контейнеру DI позаботиться о его настройке».

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

Рассмотрим альтернативу (это будет C #, потому что я знаю это лучше, но, вероятно, в Java есть точный эквивалент):

public class VeracodeService
{        
    private readonly IResultsAPIWrapper _resultsApiWrapper;
    private readonly IAdminAPIWrapper _adminApiWrapper;
    private readonly IUploadAPIWrapper _uploadApiWrapper;
    private readonly IMitigationAPIWrapper _mitigationApiWrapper;

    // Constructor
    public VeracodeService(IResultsAPIWrapper resultsApiWrapper, IAdminAPIWrapper adminApiWrapper, IUploadAPIWrapper uploadApiWrapper,         IMitigationAPIWrapper mitigationApiWrapper)
    {
         _resultsAPIWrapper = resultsAPIWrapper;
         _adminAPIWrapper = adminAPIWrapper;
         _uploadAPIWrapper = uploadAPIWrapper;
         _mitigationAPIWrapper = mitigationAPIWrapper;
    }
}

Я уже знаю кое-что об этом классе. Это неизменный класс; состояние может быть установлено только в конструкторе (ссылки, в данном конкретном случае). И поскольку все происходит от интерфейса, я могу поменять реализации в конструкторе, в который входят ваши издевательства.

Теперь все, что должен делать мой DI-контейнер, - это размышлять над конструктором, чтобы определить, какие объекты ему нужны для создания нового. Но это размышление делается на публичного члена, первоклассным способом; т. е. метаданные уже являются частью класса, поскольку они были объявлены в конструкторе, методе, целью которого является предоставление классу необходимых ему зависимостей.

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

Роберт Харви
источник
Если я не прочитал ваш пост неправильно, вы на самом деле не смотрите на пример должным образом. Моя реализация VeracodeServiceпочти идентична той, что вы написали (хотя в Java и C #). На VeracodeServiceImplTestсамом деле это класс модульного теста. В @Injectableполях, по существу , издевались объекты, вставленным в контекст. @TestedПоле объект / класс с DI конструктор , определенный. Я согласен с вашей точкой зрения, которая предпочитает инжектор конструктора, чем инжекцию в поле. Однако, как я уже упоминал, автор JMockit чувствует обратное, и я пытаюсь понять, почему
Эрик Б.
Если вы посмотрите на первый пост в этом обсуждении, вы увидите, что у меня почти такой же defn в моем классе репо.
Эрик Б.
Если вы говорите только о тестовых классах, это может не иметь значения. Потому что, тестовые занятия.
Роберт Харви,
Нет, я не говорю о тестовых классах. Я говорю о производственных классах. Просто так получается, что пример в дискуссионной группе - это модульное тестирование, так как фреймворк - это фальшивый / тестовый фреймворк. (Вся дискуссия вращается вокруг того, должна ли фреймворк поддерживать инжектор конструктора)
Эрик Б.
3
+1: да, никакой магии в версии C #. Это стандартный класс, у него есть зависимости, все они вводятся в одной точке перед использованием класса. Класс может использоваться с контейнером DI или нет. Ничто в классе не говорит, что это IOC или «Автоматическое внедрение зависимости».
Binary Worrier
27

Аргумент о меньшем количестве инициализации теста верен, но есть и другие проблемы, которые необходимо учитывать. Прежде всего, вы должны ответить на вопрос:

Хочу ли я, чтобы мой класс был инстанцируемым только с отражением?

Использование полевых инъекций означает сужение совместимости класса со средами внедрения зависимостей, которые создают объекты с помощью отражения и поддерживают эти конкретные аннотации внедрения. Некоторые платформы, основанные на языке Java, даже не поддерживают рефлексию ( GWT ), поэтому внедренный в поле класс не будет совместим с ними.

Вторая проблема - производительность . Вызов конструктора (прямой или с помощью отражения) всегда выполняется быстрее, чем набор назначений полей отражения. Платформы внедрения зависимостей должны использовать анализ отражений для построения дерева зависимостей и создания конструкторов отражений. Этот процесс вызывает дополнительное снижение производительности.

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

Все это поднимает много новых вопросов:

  1. Какова вероятность использования частей кода на другой платформе?
  2. Сколько юнит-тестов будет написано?
  3. Как быстро мне нужно готовить каждый новый релиз?
  4. ...

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

Много, много аргументов против внедрения поля.

Мацей Халапук
источник
3
+1 Ты ударил нерв. Мне не нравится, когда для создания класса требуется рефлексия или структура DI / IoC. Это как ехать в Рим, чтобы попасть в Париж из Амстердама. Имейте в виду, я люблю DI. Просто не уверен насчет рамок.
Марьян Венема
1
Великие аргументы. Это озвучивает многие мысли, которые у меня были у меня самого, но я счастлив видеть, что другие чувствуют то же самое. Как бы удивительно я ни находил полевую инъекцию, я думаю, что она мне так понравилась просто потому, что это было «проще». Я нахожу конструктор инъекций намного яснее.
Эрик Б.
19

Полевая инъекция получает определенное «нет» от меня.

Как и Роберт Харви, на мой вкус он слишком автоматичен. Я предпочитаю явный код, а не неявный, и допускаю косвенное обращение только в том случае, если / когда оно дает явные преимущества, так как затрудняет понимание и анализ кода.

Как и Maciej Chałapuk, мне не нравится рефлексия или структура DI / IoC для создания экземпляра класса. Это как ехать в Рим, чтобы попасть в Париж из Амстердама.

Имейте в виду, я люблю DI и все преимущества, которые он приносит. Просто не уверен в необходимости фреймворков для создания экземпляра класса. Особенно то, что контейнеры IoC или другие структуры DI могут влиять на тестовый код.

Мне нравится, когда мои тесты очень просты. Мне не нравится проходить через косвенную настройку контейнеров IoC или другой инфраструктуры DI, чтобы потом настроить тестируемый класс. Это просто неловко.

И это делает мои тесты зависимыми от глобального состояния фреймворка. То есть я больше не могу запускать два теста параллельно, которые требуют установки экземпляра класса X с различными зависимостями.

По крайней мере, с внедрением конструктора и сеттера у вас есть возможность не использовать каркасы и / или отражения в ваших тестах.

Марьян Венема
источник
Если вы используете, например, Mockito mockito.org , вам не понадобится интегрированная среда DI / IoC даже при использовании полевого ввода, и вы можете запустить тестирование параллельно и так далее.
Ганс-Петер Стёрр,
1
@hstoerr Отлично. Однако это должно означать, что Mockito использует отражение (или как бы это ни называлось в Java-эквиваленте) ...
Марьян Венема
5

Ну, это похоже на полемику. Первое, на что нужно обратить внимание, это то, что инъекция поля отличается от инъекции сеттера . Я уже работаю с некоторыми людьми, которые думают, что инъекция Поля и Сеттера - то же самое.

Итак, чтобы быть ясным:

Полевая инъекция:

@Autowired
private SomeService someService;

Сеттер впрыска:

@Autowired
public void setSomeService(SomeService someService) {
    this.someService = someService;
}

Но для меня они разделяют те же проблемы. И, мой любимый, конструктор инъекций:

@AutoWired
public MyService(SomeService someService) {
    this.someService = someService;
}

У меня есть следующие причины полагать, что внедрение в конструктор лучше, чем внедрение в метод / поле (я процитирую некоторые ссылки Spring, чтобы продемонстрировать свою точку зрения):

  1. Предотвращение циклических зависимостей : Spring может обнаруживать циклические зависимости между компонентами, как объяснил @Tomasz. Смотрите также Spring Team, которая говорит это.
  2. Зависимости класса очевидны : зависимости конструктора очевидны в конструкторе, но скрыты при внедрении поля / сеттера. Я чувствую, что мои занятия похожи на «черный ящик» с инъекцией поля / сеттера. И есть (по моему опыту) тенденция к тому, чтобы разработчики не беспокоились о классе с множеством зависимостей, что очевидно, когда вы пытаетесь смоделировать класс с помощью внедрения конструктора (см. Следующий пункт). Проблема, которая беспокоит разработчиков, скорее всего, будет решена. Кроме того, класс с слишком большим количеством зависимостей, вероятно, нарушает принцип единой ответственности (SRP).
  3. Легко и надежно моделировать : по сравнению с полевым внедрением , легче моделировать в модульном тесте, потому что вам не нужна никакая контекстная имитация или техника отражения для достижения объявленного частного Бина внутри класса, и вы не будете обмануты этим . Вы просто создаете экземпляр класса и передаете надруганные бобы. Просто для понимания и легко.
  4. Инструменты качества кода могут вам помочь : такие инструменты, как Sonar, могут предупредить вас о том, что класс становится слишком сложным, потому что класс с большим количеством параметров, вероятно, слишком сложный класс и требует некоторого рефакторинга. Эти инструменты не могут идентифицировать ту же проблему, когда класс использует инъекцию Field / Setter.
  5. Лучший и независимый код : с меньшим @AutoWiredраспространением среди вашего кода, ваш код меньше зависит от платформы. Я знаю, что возможность простого изменения структуры DI в проекте не оправдывает это (кто это делает, верно?). Но это показывает, для меня лучший код (я узнал об этом в трудном пути с EJB и поисков). А с помощью Spring вы можете даже удалить @AutoWiredаннотацию, используя инжектор конструктора.
  6. Больше аннотаций не всегда правильный ответ : Для некоторых причин , я действительно пытаюсь использовать аннотации только тогда , когда они действительно полезны.
  7. Вы можете сделать уколы неизменными . Неизменность - очень желанная особенность.

Если вам нужна дополнительная информация, я рекомендую эту старую (но все еще актуальную) статью из Spring Blog, рассказывающую, почему они используют так много инъекций сеттера, и рекомендацию использовать инжектор конструктора:

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

И эта заметка о том, почему они считают, что конструктор больше подходит для кода приложения:

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

Последние мысли

Если вы не совсем уверены в преимуществах конструктора, возможно, идея смешать установщик (а не поле) с инжектором конструктора - это вариант, как объяснено командой Spring :

Поскольку вы можете смешивать DI на основе конструктора и сеттера, рекомендуется использовать аргументы конструктора для обязательных зависимостей и сеттеры для необязательных зависимостей

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

Dherik
источник
-5

Может ли ваша зависимость измениться в течение жизни класса? Если это так, используйте инъекцию поля / свойства. В противном случае используйте конструктор инъекций.

Даррен Янг
источник
2
Это на самом деле ничего не объясняет. Зачем вводить приватные поля, если вы можете сделать это правильно?
Роберт Харви
Где это говорит что-нибудь о частных полях? Я говорю об общедоступном интерфейсе класса /
Даррен Янг
В этом вопросе нет аргументов о внедрении метода или внедрения конструктора. Автор JMockit видит как плохое. Посмотрите на заголовок вопроса.
Роберт Харви
Разве в вопросе не говорится, что инжекция поля против инжектора конструктора?
Даррен Янг
1
Полевая инъекция - это совершенно другое животное. Вы вкладываете ценности в частных пользователей .
Роберт Харви