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


151

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

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

Ответы:


126

Смотря как :-).

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

Если класс может работать без зависимости, установщик в порядке.


11
Я думаю, что во многих случаях предпочтительнее использовать шаблон Null Object и придерживаться требования ссылки на конструктор. Это позволяет избежать всей проверки на ноль и повышенной цикломатической сложности.
Марк Линделл

3
@ Марк: Хороший вопрос. Однако вопрос был о добавлении зависимости к существующему классу. Тогда сохранение конструктора без аргументов обеспечивает обратную совместимость.
Слеск

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

@ Патрик: «Класс не может выполнять свою работу без зависимости», я имел в виду, что разумных значений по умолчанию нет (например, класс требует подключения к БД). В вашей ситуации оба будут работать. Я все еще обычно выбираю подход конструктора, потому что он уменьшает сложность (что, если, например, сеттер вызывается дважды)?
Слёске

1
Хорошая рецензия на это здесь. Explorejava.com/different-types-of-bean-injection-in-spring
Элдхоз Авраам,

21

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

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

public class ClassExample
{
    public ClassExample(IDependencyOne dependencyOne, IDependencyTwo dependencyTwo)
        : this (dependnecyOne, dependencyTwo, new DependnecyThreeConcreteImpl())
    { }

    public ClassExample(IDependencyOne dependencyOne, IDependencyTwo dependencyTwo, IDependencyThree dependencyThree)
    {
        // Set the properties here.
    }
}

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


Конечно, цепочка конструктора работает только при наличии разумного значения по умолчанию для нового параметра. Но в противном случае вы не можете не сломать вещи ...
Sleske

Обычно в качестве параметра по умолчанию вы использовали бы все, что использовали в методе до внедрения зависимости. В идеале это сделало бы добавление нового конструктора чистым рефакторингом, поскольку поведение класса не изменилось бы.
Доктор Синий

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

17

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

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


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

11

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


11

Общий предпочтительный подход заключается в максимально возможном использовании инжектора конструктора.

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

Старайтесь иметь только один конструктор, он сохраняет дизайн простым и позволяет избежать двусмысленности (если не для людей, для контейнера DI).

Вы можете использовать внедрение свойств, когда у вас есть то, что Марк Симанн называет локальным значением по умолчанию в своей книге «Внедрение зависимостей в .NET»: зависимость является необязательной, поскольку вы можете обеспечить работоспособную реализацию, но хотите, чтобы вызывающая сторона указывала другую, если необходимо.

(Бывший ответ ниже)


Я думаю, что инъекция в конструктор лучше, если инъекция обязательна. Если это добавляет слишком много конструкторов, рассмотрите возможность использования фабрик вместо конструкторов.

Инъекция сеттера хороша, если инъекция не обязательна, или если вы хотите изменить ее на полпути. Я вообще не люблю сеттеров, но это дело вкуса.


Я бы сказал, что обычно изменение инъекции на полпути - это плохой стиль (потому что вы добавляете скрытое состояние в свой объект). Но нет правила без исключения, конечно ...
Sleske

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

«Если это добавляет слишком много конструкторов, рассмотрите возможность использования фабрик вместо конструкторов». Вы в основном откладываете любые исключения во время выполнения и можете даже ошибиться и в итоге застрять с реализацией локатора сервиса.
MeTitus

@ Марко, это мой прежний ответ, и ты прав. Если конструкторов много, я бы сказал, что класс делает слишком много вещей :-) Или рассмотрим абстрактную фабрику.
Филипп

7

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

Пока интерфейс классов (API) понятен в том, что ему нужно для выполнения своей задачи, у вас все хорошо.


уточните, пожалуйста, почему вы голосуете?
nkr1pt

3
Да, конструкторы с большим количеством аргументов плохие. Вот почему вы реорганизуете классы с большим количеством параметров конструктора :-).
Слеське

10
@ nkr1pt: Большинство людей (включая меня) соглашаются с тем, что инъекция сеттера плоха, если позволяет создать класс, который завершается неудачей во время выполнения, если инъекция не выполнена. Я полагаю, что кто-то возразил против вашего утверждения, что это личный вкус.
Слеське

7

Я лично предпочитаю «шаблон» « Извлечь и переопределить », а не вводить зависимости в конструктор, в основном по причине, изложенной в вашем вопросе. Вы можете установить свойства как virtualи затем переопределить реализацию в производном тестируемом классе.


1
Я думаю, что официальное название шаблона - «Метод шаблона».
Давиджнельсон

6

Я предпочитаю внедрение конструктора, потому что оно помогает «обеспечить» требования к зависимости класса. Если это в c'or, потребитель должен установить объекты, чтобы приложение компилировалось. Если вы используете сеттерную инъекцию, они могут не знать, что у них есть проблема, до времени выполнения - и в зависимости от объекта это может быть поздно во время выполнения.

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


6

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

Я также использую инъекцию свойств для установки вещей, на которые у контейнера нет ссылок, таких как представление ASP.NET на презентаторе, созданном с использованием контейнера.

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


Спасибо за Ваш ответ. Кажется, что конструктор - это популярный ответ. Однако я думаю, что это каким-то образом нарушает инкапсуляцию. До внедрения зависимостей класс объявляет и создает экземпляры любых конкретных типов, которые ему необходимы для выполнения своей работы. С DI подклассы (и любые ручные экземпляры) теперь знают, какие инструменты использует базовый класс. Вы добавляете новую зависимость, и теперь вам нужно связать экземпляр через все подклассы, даже если они сами не нуждаются в использовании зависимости.
Найл Коннотон

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

3

Один из вариантов, который может стоить рассмотреть, - это составление сложных множественных зависимостей из простых одиночных зависимостей. То есть определить дополнительные классы для составных зависимостей. Это немного упрощает внедрение конструктора WRT - меньшее количество параметров на вызов - и в то же время поддерживает необходимость обязательного предоставления всех зависимостей для создания экземпляра.

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

Лично я больше фанат использования методов / установщиков свойств для определения зависимостей, опций и т. Д. Имена вызовов помогают описать происходящее. Хорошей идеей будет привести примеры фрагментов «это как установить» и убедиться, что зависимый класс выполняет достаточное количество проверок ошибок. Возможно, вы захотите использовать модель конечного состояния для установки.


3

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

Использование необязательных параметров может быть выполнено только в средах, которые их поддерживают, таких как .NET 4 (как для C #, так и для VB.NET, хотя в VB.NET они всегда были). Конечно, вы можете реализовать аналогичную функциональность, просто используя свойство, которое может быть переназначено потребителем вашего класса, но вы не получаете преимущества неизменности, обеспечиваемого наличием объекта частного интерфейса, назначенного параметру конструктора.

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


1

Это старый пост, но если он понадобится в будущем, возможно, он будет полезен:

https://github.com/omegamit6zeichen/prinject

У меня была похожая идея, и я придумал эту структуру. Это, вероятно, далеко от завершения, но это идея структуры, ориентированной на внедрение свойств


0

Это зависит от того, как вы хотите реализовать. Я предпочитаю внедрение в конструктор везде, где чувствую, что значения, которые входят в реализацию, меняются не часто. Например: если стратегия compnay идет с сервером oracle, я настрою значения моего источника данных для соединения bean-компонента, получающего соединения посредством внедрения в конструктор. В противном случае, если мое приложение является продуктом и, скорее всего, оно может подключиться к любой базе данных клиента, я бы реализовал такую ​​конфигурацию базы данных и реализацию нескольких брендов посредством внедрения сеттера. Я только что взял пример, но есть более эффективные способы реализации сценариев, которые я упомянул выше.


1
Даже когда я пишу код самостоятельно, я всегда пишу код с точки зрения того, что я являюсь независимым подрядчиком, разрабатывающим код, предназначенный для распространения. Я предполагаю, что это будет с открытым исходным кодом. Таким образом, я хочу убедиться, что код является модульным и подключаемым и соответствует принципам SOLID.
Фред

0

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.