Когда не следует использовать шаблон внедрения зависимостей?

124

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

Том Сквайрс
источник
1
Связанные (вероятно, не дубликаты) - stackoverflow.com/questions/2407540/…
Steve314
3
Звучит немного как анти-паттерн "Золотой молот". ru.wikipedia.org/wiki/Anti-pattern
Cuga

Ответы:

123

По сути, внедрение зависимостей делает некоторые (обычно, но не всегда действительные) предположения о природе ваших объектов. Если это не так, DI может быть не лучшим решением:

  • Во-первых, в основном DI предполагает, что тесная связь реализаций объектов ВСЕГДА плоха . В этом суть принципа обращения зависимостей: «никогда не следует зависеть от конкреции, только от абстракции».

    Это закрывает зависимый объект для изменения на основе изменения конкретной реализации; класс, зависящий от ConsoleWriter, в частности, должен будет измениться, если вместо вывода нужно перейти в файл, но если класс зависел только от IWriter, предоставляющего метод Write (), мы можем заменить ConsoleWriter, который в настоящее время используется, на FileWriter, и наш зависимый класс не будет знать разницу (принцип замещения Лискова).

    Однако дизайн НИКОГДА не может быть закрыт для всех типов изменений; если дизайн самого интерфейса IWriter изменяется, чтобы добавить параметр в Write (), необходимо изменить дополнительный объект кода (интерфейс IWriter) поверх объекта / метода реализации и его использования (й). Если изменения в реальном интерфейсе более вероятны, чем изменения в реализации указанного интерфейса, слабая связь (и слабозависимая зависимость DI) может вызвать больше проблем, чем решает.

  • Во-вторых, и следствие, DI предполагает, что зависимый класс НИКОГДА не является хорошим местом для создания зависимости . Это относится к принципу единой ответственности; если у вас есть код, который создает зависимость и также использует ее, то есть две причины, по которым зависимому классу может потребоваться изменить (изменение в использовании или реализации), нарушая SRP.

    Однако, опять же, добавление слоев косвенности для DI может стать решением проблемы, которой не существует; если логично заключить логику в зависимость, но эта логика является единственной такой реализацией зависимости, то более болезненно кодировать слабосвязанное разрешение зависимости (внедрение, расположение службы, фабрика), чем это было бы просто использовать newи забыть об этом.

  • Наконец, DI по своей природе централизует знание всех зависимостей и их реализаций . Это увеличивает количество ссылок, которые должна иметь сборка, которая выполняет внедрение, и в большинстве случаев НЕ уменьшает количество ссылок, требуемых фактическими сборками зависимых классов.

    SOMETHING, SOMEWHERE, должен обладать знаниями о зависимом объекте, интерфейсе зависимостей и реализации зависимостей, чтобы «соединить точки» и удовлетворить эту зависимость. DI стремится разместить все эти знания на очень высоком уровне, либо в контейнере IoC, либо в коде, который создает «основные» объекты, такие как основная форма или контроллер, которые должны гидратировать (или предоставлять фабричные методы) зависимости. Это может поместить много обязательно тесно связанного кода и множество ссылок на сборки на высоких уровнях вашего приложения, которым нужны только эти знания, чтобы «спрятать» их от реальных зависимых классов (что с очень простой точки зрения является лучшее место, чтобы иметь эти знания; где они используются).

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

    • все в одной сборке «Интерфейсы», которая становится очень ориентированной на приложения,
    • каждая из них вместе с основной (-ыми) реализацией (-ями), устраняя преимущество отсутствия необходимости перекомпиляции зависимостей при изменении зависимостей, или
    • один или два компонента в высоко связных сборках, что увеличивает количество сборок, значительно увеличивает время «полной сборки» и снижает производительность приложения.

    Все это снова для решения проблемы в местах, где их может не быть.

Keiths
источник
1
SRP = принцип единой ответственности, для всех, кто интересуется.
Теодор Мердок
1
Да, и LSP - это принцип подстановки Лискова; учитывая зависимость от A, зависимость должна быть в состоянии встретить B, который происходит от A без каких-либо других изменений.
KeithS
Внедрение зависимостей особенно помогает в тех случаях, когда вам нужно получить класс A из класса B из класса D и т. д. В этом случае (и только в этом случае) структура DI может генерировать меньше разборки сборки, чем инъекция бедного человека. Кроме того, у меня никогда не было узкого места, вызванного DI. Подумайте о затратах на обслуживание, а не о затратах на процессор, потому что код всегда можно оптимизировать, но делать это без причины стоит
GameDeveloper
Будет ли другое предположение «если зависимость меняется, она меняется везде»? Или же вы все равно должны посмотреть на все потребительские классы
Ричард Тингл
3
В этом ответе не учитывается влияние внедрения зависимостей на тестируемость по сравнению с их жестким кодированием. См. Также принцип явных зависимостей ( deviq.com/explicit-dependencies-principle )
ссмит
30

Вне рамок внедрения зависимостей внедрение зависимостей (через внедрение конструктора или установщика) является почти игрой с нулевой суммой: вы уменьшаете связь между объектом A и его зависимостью B, но теперь любой объект, которому требуется экземпляр A, должен теперь также построить объект B.

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

Таким образом, внедрение зависимости (без фреймворка) примерно так же вредно, как и полезно.

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

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

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

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

В структуре внедрения зависимости компромиссы немного отличаются; что вы теряете, внедряя зависимость - это способность легко знать, на какую реализацию вы полагаетесь, и перекладывать ответственность за решение, на какую зависимость вы полагаетесь, на какой-то автоматический процесс разрешения (например, если нам требуется @ Inject'ed Foo , должно быть что-то, что @Provides Foo, и чьи внедренные зависимости доступны), или какой-то высокоуровневый файл конфигурации, который предписывает, какого поставщика следует использовать для каждого ресурса, или некоторому гибриду из этих двух (например, может быть автоматическим процессом разрешения зависимостей, которые при необходимости можно переопределить с помощью файла конфигурации).

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

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

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

Теодор Мердок
источник
6
Просто краткий комментарий о проверке зависимостей в тестах и ​​«вы должны знать о зависимостях зависимостей ваших зависимостей» ... Это совсем не так. Не нужно знать или заботиться о зависимости конкретной реализации, если вводится макет. Нужно только знать о прямых зависимостях тестируемого класса, которые удовлетворяются макетом (ами).
Эрик Кинг,
6
Вы неправильно понимаете, я не говорю о том, когда вы вводите макет, я говорю о реальном коде. Рассмотрим класс A с зависимостью B, которая, в свою очередь, имеет зависимость C, которая, в свою очередь, имеет зависимость D. Без DI конструкции A конструируют B, B конструируют C, C конструируют D. С внедрением конструкции для построения A сначала необходимо создать B, чтобы построить B, вы должны сначала построить C, а чтобы построить C, вы должны сначала построить D. Поэтому класс A теперь должен знать о D, зависимости зависимости от зависимости, чтобы построить B. Это приводит к чрезмерной связи.
Теодор Мердок
1
Не было бы так дорого иметь дополнительный конструктор, используемый только для тестирования, который позволяет вводить зависимости. Я постараюсь пересмотреть то, что я сказал.
Теодор Мердок
3
Внедрение зависимостей не перемещает зависимости только в игре с нулевой суммой. Вы перемещаете свои зависимости в места, где они уже существуют: основной проект. Вы даже можете использовать подходы, основанные на соглашениях, чтобы убедиться, что зависимости разрешаются только во время выполнения, например, указав, какие классы являются конкретными по пространствам имен или как угодно. Я должен сказать, что это наивный ответ
Sprague
2
@Sprague Внедрение зависимостей перемещает зависимости в игре с небольшой отрицательной суммой, если вы применяете ее в тех случаях, когда ее не следует применять. Цель этого ответа - напомнить людям, что внедрение зависимостей не является по своей сути хорошим, это шаблон проектирования, который невероятно полезен в правильных обстоятельствах, но неоправданно затратный в нормальных условиях (по крайней мере, в областях программирования, в которых я работал Я понимаю, что в некоторых доменах это более полезно, чем в других). DI иногда становится модным словом и самоцелью, которая упускает суть.
Теодор Мердок
11
  1. если вы создаете объекты базы данных, вы должны иметь некоторый фабричный класс, который вы будете внедрять вместо этого в свой контроллер,

  2. если вам нужно создать примитивные объекты, такие как целые или длинные. Также вы должны создавать «вручную» большинство стандартных объектов библиотеки, таких как даты, руководства и т. Д.

  3. если вы хотите внедрить конфигурационные строки, возможно, лучше внедрить некоторые конфигурационные объекты (в общем случае рекомендуется заключать простые типы в значимые объекты: int TemperatureInCelsiusDegrees -> CelciusDeegree Temperature)

И не используйте локатор служб в качестве альтернативы внедрения зависимостей, это анти-шаблон, больше информации: http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx

0lukasz0
источник
Все пункты о том, чтобы использовать другую форму инъекции, а не использовать ее полностью. +1 за ссылку, хотя.
Ян Худек
3
Сервисный локатор НЕ является анти-паттерном, и парень, на блоге которого вы ссылаетесь, не является экспертом в паттернах. То, что он получил StackExchange-известность, является своего рода медвежьей услугой в разработке программного обеспечения. Мартин Фаулер (автор основных шаблонов архитектуры корпоративных приложений) имеет более разумное мнение по этому вопросу: martinfowler.com/articles/injection.html
Колин,
1
@Colin, напротив, Service Locator, безусловно, является анти-паттерном, и здесь в двух словах причина .
Киралесса
@Kyralessa - вы понимаете, что структуры DI внутренне используют шаблон Service Locator для поиска сервисов, верно? Существуют обстоятельства, когда оба подходят, и ни один из них не является анти-паттерном, несмотря на ненависть StackExchange, которую некоторые люди хотят выбросить.
Колин
Конечно, и моя машина использует поршни и свечи зажигания и множество других вещей, которые, однако, не являются хорошим интерфейсом для обычного человека, который просто хочет водить машину.
Kyralessa
8

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

Серьезно, я люблю IoC и DI в целом, и я бы сказал, что в 98% случаев я буду использовать этот шаблон в обязательном порядке. Это особенно важно в многопользовательской среде, где ваш код может снова и снова использоваться разными членами команды и разными проектами, поскольку это отделяет логику от реализации. Ведение журнала является ярким примером того, что интерфейс ILog, внедренный в класс, в тысячу раз проще в обслуживании, чем простое подключение вашей среды ведения журнала-du-jour, так как у вас нет гарантии, что другой проект будет использовать ту же структуру ведения журнала (если он использует один на всех!).

Однако бывают случаи, когда это не применимо. Например, функциональные точки входа, которые реализуются в статическом контексте не перезаписываемым инициализатором (WebMethods, я смотрю на вас, но ваш метод Main () в вашем классе Program - еще один пример) просто не может иметь зависимости, введенные при инициализации время. Я бы также сказал, что прототип или любой фрагмент кода, который можно было бы выбросить, также является плохим кандидатом; Преимущества DI в значительной степени среднесрочные и долгосрочные (тестируемость и ремонтопригодность), если вы уверены, что вы выбросите большую часть кода в течение недели или около того, я бы сказал, что вы ничего не получите, изолируя зависимости, просто потратьте время, которое вы обычно проводите на тестирование и изоляцию зависимостей, чтобы код работал.

В целом, имеет смысл принять прагматичный подход к любой методологии или модели, поскольку ничто не применимо в 100% случаев.

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

Эд Джеймс
источник
Я не понимаю, что вы имеете в виду под ведением журнала. Конечно, все регистраторы не будут соответствовать одному и тому же интерфейсу, поэтому вам придется написать свою собственную оболочку для перевода между журналами этого проекта и определенным регистратором (при условии, что вы хотите иметь возможность легко менять регистраторы). Так что же после этого тебе дает ДИ?
Ричард Тингл
@RichardTingle Я бы сказал, что вы должны определить свою оболочку журналирования как интерфейс, а затем вы пишете облегченную реализацию этого интерфейса для каждой внешней службы журналирования, вместо того, чтобы использовать один класс журналирования, который оборачивает несколько абстракций. Это та же концепция, которую вы предлагаете, но абстрагированная на другом уровне.
Эд Джеймс
1

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

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

  1. Там должна быть причина, чтобы ввести это.

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

  2. Команда должна быть обучена и на борту.

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

    Если DI введен новым членом команды, потому что он понимает его и ему нравится и просто хочет показать, что он хорош, И команда не принимает активного участия, существует реальный риск того, что он фактически снизит качество код.

  3. Вам нужно проверить

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

tymtam
источник
1

Это не полный ответ, а просто еще один момент.

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

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

Корай Тугай
источник
Я не вижу, как в прямом эфире время приложения, связанного с DI
Майкл Фрайдгейм
@MichaelFreidgeim Требуется время для инициализации контекста, обычно контейнеры DI довольно тяжелые, такие как Spring. Создайте приложение hello world с одним классом, создайте одно с помощью Spring и запустите оба 10 раз, и вы поймете, что я имею в виду.
Корай Тугай
Это звучит как проблема с отдельным контейнером DI. В мире .Net я не слышал о времени инициализации как о существенной проблеме для контейнеров DI
Майкл Фрейдгейм
1

Попробуйте использовать базовые принципы ООП: использовать наследование для извлечения общей функциональности, инкапсулировать (скрывать) вещи, которые должны быть защищены от внешнего мира, используя закрытые / внутренние / защищенные члены / типы. Используйте любую мощную тестовую среду для внедрения кода только для тестов, например https://www.typemock.com/ или https://www.telerik.com/products/mocking.aspx .

Затем попробуйте переписать его с DI и сравнить код, который вы обычно видите с DI:

  1. У вас есть больше интерфейсов (больше типов)
  2. Вы создали дубликаты подписей общедоступных методов, и вам нужно будет дважды его обслуживать (вы не можете просто изменить один параметр один раз, вам придется сделать это дважды, в основном все возможности рефакторинга и навигации становятся более сложными)
  3. Вы переместили некоторые ошибки компиляции во время сбоев во время выполнения (с помощью DI вы можете просто игнорировать некоторую зависимость во время кодирования и не быть уверенным, что она будет обнаружена во время тестирования)
  4. Вы открыли свою инкапсуляцию. Теперь защищенные члены, внутренние типы и т. Д. Стали общедоступными
  5. Общий объем кода был увеличен

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

Однако, если вы используете только «публичный» модификатор доступа в объявлении класса и / или публичные / приватные модификаторы для членов, и / или у вас нет выбора покупать дорогие тестовые инструменты, и в то же время вам нужно модульное тестирование, которое может ' Если вас не заменит интеграционное тестирование, и / или у вас уже есть интерфейсы для классов, которые вы хотите внедрить, DI - хороший выбор!

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

Евгений Горбовой
источник
0

Альтернатива Dependency Injection использует Service Locator . Сервисный локатор проще для понимания, отладки и упрощает конструирование объекта, особенно если вы не используете инфраструктуру DI. Сервисные локаторы - хороший шаблон для управления внешними статическими зависимостями , например, база данных, которую в противном случае вам пришлось бы передавать в каждый объект на уровне доступа к данным.

При рефакторинге унаследованного кода часто проще выполнить рефакторинг в Service Locator, чем в Dependency Injection. Все, что вы делаете, это заменяете экземпляры поиском службы, а затем подделываете службу в своем модульном тесте.

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

TLDR : если у вашего класса есть статические зависимости или вы рефакторинге унаследованного кода, Service Locator, возможно, лучше, чем DI.

Гарретт Холл
источник
12
Сервисный локатор скрывает зависимости в вашем коде . Это не хороший шаблон для использования.
Kyralessa
Когда дело доходит до статических зависимостей, я бы предпочел, чтобы на них накладывали фасады, которые реализуют интерфейсы. Затем они могут быть введены зависимостью, и они падают на статическую гранату для вас.
Эрик Дитрих
@ Kyralessa Я согласен, сервисный локатор имеет много недостатков, и DI почти всегда предпочтительнее. Тем не менее, я считаю, что есть пара исключений из правила, как и со всеми принципами программирования.
Гарретт Холл
Основное допустимое использование местоположения службы находится в шаблоне «Стратегия», где классу «выбора стратегии» предоставляется достаточно логики, чтобы найти стратегию для использования и либо передать ее обратно, либо передать ей вызов. Даже в этом случае средство выбора стратегии может быть фасадом для фабричного метода, предоставляемого контейнером IoC, который имеет ту же логику; причина, по которой вы бы это сломали, заключается в том, чтобы поместить логику туда, где она принадлежит, а не туда, где она наиболее скрыта.
KeithS
По словам Мартина Фаулера, «выбор между (DI и Service Locator) менее важен, чем принцип отделения конфигурации от использования». Идея, что DI ВСЕГДА лучше, это фигня. Если служба используется глобально, использовать DI более громоздко и хрупко. Кроме того, DI менее благоприятен для плагиновых архитектур, где реализации неизвестны до времени выполнения. Подумайте, как неловко было бы всегда передавать дополнительные аргументы в Debug.WriteLine () и тому подобное!
Колин