С тех пор, как я начал изучать (и любить) автоматизированное тестирование, я обнаружил, что использую шаблон внедрения зависимостей почти в каждом проекте. Всегда ли уместно использовать этот шаблон при работе с автоматизированным тестированием? В каких ситуациях вам следует избегать использования инъекций зависимостей?
design-patterns
dependency-injection
Том Сквайрс
источник
источник
Ответы:
По сути, внедрение зависимостей делает некоторые (обычно, но не всегда действительные) предположения о природе ваших объектов. Если это не так, DI может быть не лучшим решением:
Во-первых, в основном DI предполагает, что тесная связь реализаций объектов ВСЕГДА плоха . В этом суть принципа обращения зависимостей: «никогда не следует зависеть от конкреции, только от абстракции».
Это закрывает зависимый объект для изменения на основе изменения конкретной реализации; класс, зависящий от ConsoleWriter, в частности, должен будет измениться, если вместо вывода нужно перейти в файл, но если класс зависел только от IWriter, предоставляющего метод Write (), мы можем заменить ConsoleWriter, который в настоящее время используется, на FileWriter, и наш зависимый класс не будет знать разницу (принцип замещения Лискова).
Однако дизайн НИКОГДА не может быть закрыт для всех типов изменений; если дизайн самого интерфейса IWriter изменяется, чтобы добавить параметр в Write (), необходимо изменить дополнительный объект кода (интерфейс IWriter) поверх объекта / метода реализации и его использования (й). Если изменения в реальном интерфейсе более вероятны, чем изменения в реализации указанного интерфейса, слабая связь (и слабозависимая зависимость DI) может вызвать больше проблем, чем решает.
Во-вторых, и следствие, DI предполагает, что зависимый класс НИКОГДА не является хорошим местом для создания зависимости . Это относится к принципу единой ответственности; если у вас есть код, который создает зависимость и также использует ее, то есть две причины, по которым зависимому классу может потребоваться изменить (изменение в использовании или реализации), нарушая SRP.
Однако, опять же, добавление слоев косвенности для DI может стать решением проблемы, которой не существует; если логично заключить логику в зависимость, но эта логика является единственной такой реализацией зависимости, то более болезненно кодировать слабосвязанное разрешение зависимости (внедрение, расположение службы, фабрика), чем это было бы просто использовать
new
и забыть об этом.Наконец, DI по своей природе централизует знание всех зависимостей и их реализаций . Это увеличивает количество ссылок, которые должна иметь сборка, которая выполняет внедрение, и в большинстве случаев НЕ уменьшает количество ссылок, требуемых фактическими сборками зависимых классов.
SOMETHING, SOMEWHERE, должен обладать знаниями о зависимом объекте, интерфейсе зависимостей и реализации зависимостей, чтобы «соединить точки» и удовлетворить эту зависимость. DI стремится разместить все эти знания на очень высоком уровне, либо в контейнере IoC, либо в коде, который создает «основные» объекты, такие как основная форма или контроллер, которые должны гидратировать (или предоставлять фабричные методы) зависимости. Это может поместить много обязательно тесно связанного кода и множество ссылок на сборки на высоких уровнях вашего приложения, которым нужны только эти знания, чтобы «спрятать» их от реальных зависимых классов (что с очень простой точки зрения является лучшее место, чтобы иметь эти знания; где они используются).
Обычно он также не удаляет указанные ссылки из нижнего уровня кода; зависимый должен по-прежнему ссылаться на библиотеку, содержащую интерфейс для своей зависимости, которая находится в одном из трех мест:
Все это снова для решения проблемы в местах, где их может не быть.
источник
Вне рамок внедрения зависимостей внедрение зависимостей (через внедрение конструктора или установщика) является почти игрой с нулевой суммой: вы уменьшаете связь между объектом A и его зависимостью B, но теперь любой объект, которому требуется экземпляр A, должен теперь также построить объект B.
Вы немного уменьшили связь между A и B, но сократили инкапсуляцию A и увеличили связь между A и любым классом, который должен создать экземпляр A, также связав их с зависимостями A.
Таким образом, внедрение зависимости (без фреймворка) примерно так же вредно, как и полезно.
Однако дополнительные затраты часто легко оправданы: если клиентский код знает больше о том, как построить зависимость, чем сам объект, то внедрение зависимости действительно уменьшает связь; например, сканер не знает много о том, как получить или сконструировать входной поток для анализа ввода, или из какого источника клиентский код хочет анализировать ввод, поэтому инжекционное конструирование входного потока является очевидным решением.
Тестирование является еще одним оправданием, чтобы можно было использовать фиктивные зависимости. Это должно означать добавление дополнительного конструктора, используемого только для тестирования, который позволяет вводить зависимости: если вместо этого вы изменяете свои конструкторы так, чтобы всегда требовать внедрения зависимостей, внезапно, вам нужно знать о зависимостях зависимостей ваших зависимостей, чтобы построить ваш прямые зависимости, и вы не можете сделать какую-либо работу.
Это может быть полезно, но вы должны определенно спросить себя для каждой зависимости, стоит ли польза от тестирования стоимости, и я действительно хочу высмеивать эту зависимость во время тестирования?
Когда добавляется структура внедрения зависимостей, а построение зависимостей делегируется не клиентскому коду, а вместо этого структуре, анализ затрат и выгод значительно меняется.
В структуре внедрения зависимости компромиссы немного отличаются; что вы теряете, внедряя зависимость - это способность легко знать, на какую реализацию вы полагаетесь, и перекладывать ответственность за решение, на какую зависимость вы полагаетесь, на какой-то автоматический процесс разрешения (например, если нам требуется @ Inject'ed Foo , должно быть что-то, что @Provides Foo, и чьи внедренные зависимости доступны), или какой-то высокоуровневый файл конфигурации, который предписывает, какого поставщика следует использовать для каждого ресурса, или некоторому гибриду из этих двух (например, может быть автоматическим процессом разрешения зависимостей, которые при необходимости можно переопределить с помощью файла конфигурации).
Как и в случае с инжекцией в конструктор, я думаю, что преимущество в этом, в конечном итоге, очень похоже на стоимость: вы не должны знать, кто предоставляет данные, на которые вы полагаетесь, и, если есть несколько потенциальных провайдерам, вам не нужно знать предпочтительный порядок регистрации провайдеров, убедитесь, что каждое местоположение, которое нуждается в данных, проверяет всех потенциальных провайдеров и т. д., потому что все это обрабатывается на высоком уровне путем внедрения зависимости Платформа.
Хотя лично у меня нет большого опыта работы с DI-структурами, у меня сложилось впечатление, что они дают больше преимуществ, чем затрат, когда головная боль при поиске правильного поставщика данных или услуги, которая вам нужна, стоит дороже, чем головная боль, когда что-то не получается, не зная локально, какой код предоставил неверные данные, что вызвало последующий сбой в вашем коде.
В некоторых случаях другие шаблоны, которые скрывают зависимости (например, локаторы служб), уже были приняты (и, возможно, также доказали свою ценность), когда на сцене появились платформы DI, и платформы DI были приняты, поскольку они предлагали некоторые конкурентные преимущества, такие как требование меньше стандартного кода или, возможно, меньше, чтобы скрыть поставщика зависимостей, когда возникает необходимость определить, какой поставщик фактически используется.
источник
если вы создаете объекты базы данных, вы должны иметь некоторый фабричный класс, который вы будете внедрять вместо этого в свой контроллер,
если вам нужно создать примитивные объекты, такие как целые или длинные. Также вы должны создавать «вручную» большинство стандартных объектов библиотеки, таких как даты, руководства и т. Д.
если вы хотите внедрить конфигурационные строки, возможно, лучше внедрить некоторые конфигурационные объекты (в общем случае рекомендуется заключать простые типы в значимые объекты: int TemperatureInCelsiusDegrees -> CelciusDeegree Temperature)
И не используйте локатор служб в качестве альтернативы внедрения зависимостей, это анти-шаблон, больше информации: http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx
источник
Когда вы ничего не добьетесь, сделав свой проект обслуживаемым и тестируемым.
Серьезно, я люблю IoC и DI в целом, и я бы сказал, что в 98% случаев я буду использовать этот шаблон в обязательном порядке. Это особенно важно в многопользовательской среде, где ваш код может снова и снова использоваться разными членами команды и разными проектами, поскольку это отделяет логику от реализации. Ведение журнала является ярким примером того, что интерфейс ILog, внедренный в класс, в тысячу раз проще в обслуживании, чем простое подключение вашей среды ведения журнала-du-jour, так как у вас нет гарантии, что другой проект будет использовать ту же структуру ведения журнала (если он использует один на всех!).
Однако бывают случаи, когда это не применимо. Например, функциональные точки входа, которые реализуются в статическом контексте не перезаписываемым инициализатором (WebMethods, я смотрю на вас, но ваш метод Main () в вашем классе Program - еще один пример) просто не может иметь зависимости, введенные при инициализации время. Я бы также сказал, что прототип или любой фрагмент кода, который можно было бы выбросить, также является плохим кандидатом; Преимущества DI в значительной степени среднесрочные и долгосрочные (тестируемость и ремонтопригодность), если вы уверены, что вы выбросите большую часть кода в течение недели или около того, я бы сказал, что вы ничего не получите, изолируя зависимости, просто потратьте время, которое вы обычно проводите на тестирование и изоляцию зависимостей, чтобы код работал.
В целом, имеет смысл принять прагматичный подход к любой методологии или модели, поскольку ничто не применимо в 100% случаев.
Стоит отметить, что вы прокомментировали автоматизированное тестирование: мое определение этого термина - автоматические функциональные тесты, например скриптовые тесты селена, если вы находитесь в веб-контексте. Как правило, это полностью тесты «черного ящика», при которых не нужно знать о внутренней работе кода. Если бы вы имели в виду модульные или интеграционные тесты, я бы сказал, что шаблон DI почти всегда применим к любому проекту, который в значительной степени основан на такого рода тестах белого ящика, поскольку, например, он позволяет вам тестировать такие вещи, как методы, которые касаются БД без необходимости присутствия БД.
источник
В то время как другие ответы сосредоточены на технических аспектах, я хотел бы добавить практическое измерение.
За эти годы я пришел к выводу, что есть несколько практических требований, которые должны быть выполнены, чтобы внедрение Dependency Injection было успешным.
Там должна быть причина, чтобы ввести это.
Это звучит очевидно, но если ваш код только получает данные из базы данных и возвращает их без какой-либо логики, то добавление контейнера DI усложняет ситуацию, а не приносит реальной выгоды. Интеграционное тестирование было бы более важным здесь.
Команда должна быть обучена и на борту.
Если большая часть команды не работает и не понимает DI, добавление инверсии контейнера управления становится еще одним способом сделать вещи и еще более усложнить кодовую базу.
Если DI введен новым членом команды, потому что он понимает его и ему нравится и просто хочет показать, что он хорош, И команда не принимает активного участия, существует реальный риск того, что он фактически снизит качество код.
Вам нужно проверить
Хотя развязка - это, как правило, хорошая вещь, DI может перемещать разрешение зависимости со времени компиляции во время выполнения. Это на самом деле довольно опасно, если вы плохо тестируете. Сбои разрешения во время выполнения могут быть дорогостоящими для отслеживания и устранения.
(Из вашего теста видно, что вы проходите тестирование, но многие команды не проводят тестирование в той степени, которая требуется DI.)
источник
Это не полный ответ, а просто еще один момент.
Если у вас есть приложение, которое запускается один раз, работает долго (например, веб-приложение), DI может быть хорошим.
Если у вас есть приложение, которое запускается много раз и работает в течение более коротких периодов времени (например, мобильное приложение), вам, вероятно, не нужен контейнер.
источник
Попробуйте использовать базовые принципы ООП: использовать наследование для извлечения общей функциональности, инкапсулировать (скрывать) вещи, которые должны быть защищены от внешнего мира, используя закрытые / внутренние / защищенные члены / типы. Используйте любую мощную тестовую среду для внедрения кода только для тестов, например https://www.typemock.com/ или https://www.telerik.com/products/mocking.aspx .
Затем попробуйте переписать его с DI и сравнить код, который вы обычно видите с DI:
Я бы сказал, что почти всегда качество кода снижается с помощью DI.
Однако, если вы используете только «публичный» модификатор доступа в объявлении класса и / или публичные / приватные модификаторы для членов, и / или у вас нет выбора покупать дорогие тестовые инструменты, и в то же время вам нужно модульное тестирование, которое может ' Если вас не заменит интеграционное тестирование, и / или у вас уже есть интерфейсы для классов, которые вы хотите внедрить, DI - хороший выбор!
ps, вероятно, я получу много минусов за этот пост, я полагаю, потому что большинство современных разработчиков просто не понимают, как и почему использовать внутреннее ключевое слово и как уменьшить связывание ваших компонентов и, наконец, почему уменьшить его), наконец, просто попробуй закодировать и сравнить
источник
Альтернатива Dependency Injection использует Service Locator . Сервисный локатор проще для понимания, отладки и упрощает конструирование объекта, особенно если вы не используете инфраструктуру DI. Сервисные локаторы - хороший шаблон для управления внешними статическими зависимостями , например, база данных, которую в противном случае вам пришлось бы передавать в каждый объект на уровне доступа к данным.
При рефакторинге унаследованного кода часто проще выполнить рефакторинг в Service Locator, чем в Dependency Injection. Все, что вы делаете, это заменяете экземпляры поиском службы, а затем подделываете службу в своем модульном тесте.
Однако у сервисного локатора есть некоторые недостатки . Знать недостатки класса более сложно, потому что зависимости скрыты в реализации класса, а не в конструкторах или установщиках. А создать два объекта, которые полагаются на разные реализации одного и того же сервиса, сложно или невозможно.
TLDR : если у вашего класса есть статические зависимости или вы рефакторинге унаследованного кода, Service Locator, возможно, лучше, чем DI.
источник