Внедрение зависимостей (DI) является хорошо известным и модным паттерном. Большинство инженеров знают его преимущества, такие как:
- Обеспечение изоляции в модульном тестировании возможно / просто
- Явно определяющие зависимости класса
- Содействие хорошему дизайну (например, принцип единой ответственности )
- Быстрое включение реализаций (например,
DbLogger
вместоConsoleLogger
)
Я считаю, что в отрасли существует единодушное мнение, что DI - хороший, полезный шаблон. Сейчас не так уж много критики. Недостатки, которые упоминаются в сообществе, обычно незначительны. Некоторые из них:
- Увеличение количества классов
- Создание ненужных интерфейсов
В настоящее время мы обсуждаем архитектуру с моим коллегой. Он довольно консервативный, но открытый. Ему нравится подвергать сомнению вещи, которые я считаю хорошими, потому что многие люди в IT просто копируют новейшую тенденцию, повторяют преимущества и вообще не слишком много думают - не анализируйте слишком глубоко.
Вещи, которые я хотел бы спросить:
- Должны ли мы использовать внедрение зависимостей, когда у нас есть только одна реализация?
- Должны ли мы запретить создание новых объектов, кроме языковых / каркасных?
- Является ли внедрение одной реализации плохой идеей (скажем, у нас есть только одна реализация, поэтому мы не хотим создавать «пустой» интерфейс), если мы не планируем модульное тестирование определенного класса?
UserService
этот класс просто держатель для логики. Он вставляет соединение с базой данных и запускает тесты внутри транзакции, которая откатывается. Многие назвали бы это плохой практикой, но я обнаружил, что это работает очень хорошо. Не нужно искажать код только для тестирования, и вы получаете возможность обнаружения ошибок в интеграционных тестах.Ответы:
Во-первых, я хотел бы отделить подход к проектированию от концепции фреймворков. Внедрение зависимостей на самом простом и фундаментальном уровне - это просто:
Вот и все. Обратите внимание, что для этого ничего не требуется интерфейсов, фреймворков, любого стиля внедрения и т. Д. Чтобы быть справедливым, я впервые узнал об этом шаблоне 20 лет назад. Это не ново.
Из-за того, что более 2 человек имеют путаницу по поводу термина «родитель» и «ребенок» в контексте внедрения зависимости:
Внедрение зависимостей - это шаблон для композиции объекта .
Почему интерфейсы?
Интерфейсы - это контракт. Они существуют, чтобы ограничить, насколько тесно могут быть связаны два объекта. Не всем зависимостям нужен интерфейс, но они помогают при написании модульного кода.
Когда вы добавляете концепцию модульного тестирования, у вас могут быть две концептуальные реализации для любого данного интерфейса: реальный объект, который вы хотите использовать в своем приложении, и макетированный или заглушенный объект, который вы используете для тестирования кода, который зависит от объекта. Одного этого может быть достаточно для интерфейса.
Почему рамки?
По сути, инициализация и предоставление зависимостей дочерним объектам могут быть пугающими, когда их много. Фреймворки предоставляют следующие преимущества:
У них также есть следующие недостатки:
Все, что сказал, есть компромиссы. Для небольших проектов, в которых не так много движущихся частей, и нет особых причин использовать DI-фреймворк. Однако для более сложных проектов, в которых уже есть определенные компоненты, рамки могут быть оправданы.
Как насчет [случайной статьи в Интернете]?
Что насчет этого? Много раз люди могут переусердствовать, добавлять кучу ограничений и ругать вас, если вы не делаете «единственно верный путь». Там нет одного верного пути. Посмотрите, сможете ли вы извлечь из статьи что-нибудь полезное и игнорировать то, с чем вы не согласны.
Короче, подумай сам и попробуй.
Работа со "старыми головами"
Узнайте как можно больше. Со многими разработчиками, которые работают в свои 70 лет, вы обнаружите, что они научились не быть догматичными во многих вещах. У них есть методы, с которыми они работали на протяжении десятилетий, которые дают правильные результаты.
Я имел честь поработать с некоторыми из них, и они могут предоставить несколько откровенно честных отзывов, которые имеют большой смысл. И там, где они видят ценность, они добавляют эти инструменты в свой репертуар.
источник
Внедрение зависимостей, как и большинство моделей, является решением проблем . Поэтому начните с вопроса, есть ли у вас вообще проблема? Если нет, то с помощью шаблона , скорее всего, сделает код хуже .
Сначала подумайте, можете ли вы уменьшить или устранить зависимости. При прочих равных условиях мы хотим, чтобы каждый компонент в системе имел как можно меньше зависимостей. И если зависимости исчезли, вопрос о введении или нет становится спорным!
Рассмотрим модуль, который загружает некоторые данные из внешней службы, анализирует их, выполняет сложный анализ и записывает результаты в файл.
Теперь, если зависимость от внешнего сервиса жестко запрограммирована, тогда будет действительно сложно выполнить внутреннюю обработку этого модуля модульным тестированием. Таким образом, вы можете решить добавить внешнюю службу и файловую систему в качестве зависимостей интерфейса, что позволит вам вместо этого внедрять макеты, что в свою очередь делает возможным модульное тестирование внутренней логики.
Но гораздо лучшим решением будет просто отделить анализ от ввода / вывода. Если анализ извлекается в модуль без побочных эффектов, его будет гораздо проще протестировать. Обратите внимание, что издевательство - это запах кода - его не всегда можно избежать, но в целом лучше тестировать, не полагаясь на насмешку. Таким образом, устраняя зависимости, вы избегаете проблем, которые DI должен облегчить. Обратите внимание, что такой дизайн также лучше придерживается SRP.
Я хочу подчеркнуть, что DI не обязательно способствует SRP или другим хорошим принципам проектирования, таким как разделение проблем, высокая когезия / низкая связь и так далее. С таким же успехом это может иметь противоположный эффект. Рассмотрим класс A, который использует другой класс B. B используется только A и поэтому полностью инкапсулирован и может рассматриваться как деталь реализации. Если вы измените это, чтобы внедрить B в конструктор A, то вы раскрыли детали этой реализации, и теперь знания об этой зависимости и о том, как инициализировать B, время жизни B и т. Д. Должны существовать в каком-то другом месте в системе отдельно. От А. Таким образом, у вас общая архитектура хуже с утечками.
С другой стороны, в некоторых случаях DI действительно полезен. Например, для глобальных услуг с побочными эффектами, такими как регистратор.
Проблема в том, что шаблоны и архитектуры сами по себе становятся целями, а не инструментами. Просто спрашиваю "Должны ли мы использовать DI?" вроде ставит телегу перед лошадью. Вы должны спросить: «Есть ли у нас проблемы?» и "Как лучше всего решить эту проблему?"
Часть вашего вопроса сводится к следующему: «Должны ли мы создавать лишние интерфейсы для удовлетворения требований шаблона?» Вы, наверное, уже поняли ответ на этот вопрос - абсолютно нет ! Любой, кто говорит вам иначе, пытается продать вам что-то - скорее всего, дорогие консультационные часы. Интерфейс имеет значение, только если он представляет абстракцию. Интерфейс, который просто имитирует поверхность отдельного класса, называется «интерфейсом заголовка», и это известный антипаттерн.
источник
A
используетсяB
в производстве, но был протестирован только на соответствиеMockB
, наши тесты не говорят нам, будет ли он работать в производстве. Когда чистые (без побочных эффектов) компоненты доменной модели внедряют и высмеивают друг друга, результатом является огромная трата времени каждого, раздутая и хрупкая кодовая база и низкая уверенность в получающейся системе. Издевайтесь над границами системы, а не между произвольными частями одной и той же системы.По моему опыту, у внедрения зависимости есть ряд недостатков.
Во-первых, использование DI не настолько упрощает автоматизированное тестирование, сколько рекламируется. Модульное тестирование класса с фиктивной реализацией интерфейса позволяет проверить, как этот класс будет взаимодействовать с интерфейсом. Таким образом, он позволяет вам тестировать модули, как тестируемый класс использует контракт, предоставленный интерфейсом. Однако это обеспечивает гораздо большую уверенность в том, что ввод тестируемого класса в интерфейс соответствует ожиданиям. Это дает довольно слабую уверенность в том, что тестируемый класс реагирует так, как ожидается, на вывод из интерфейса, поскольку это почти универсальный фиктивный вывод, который сам по себе подвержен ошибкам, упрощениям и так далее. Короче говоря, это НЕ позволяет вам проверить, что класс будет вести себя так, как ожидается с реальной реализацией интерфейса.
Во-вторых, DI значительно усложняет навигацию по коду. При попытке перейти к определению классов, используемых в качестве входных данных для функций, интерфейс может быть чем угодно, от незначительного раздражения (например, при наличии единственной реализации) до большого расхода времени (например, когда используется слишком общий интерфейс, такой как IDisposable) при попытке найти фактическую реализацию используется. Это может превратить простое упражнение типа «Мне нужно исправить исключение нулевой ссылки в коде, которое происходит сразу после того, как напечатан этот оператор ведения журнала», в однодневное усилие.
В-третьих, использование DI и фреймворков - это обоюдоострый меч. Это может значительно уменьшить количество кода котельной пластины, необходимого для обычных операций. Однако это происходит за счет необходимости детального знания конкретной структуры DI, чтобы понять, как эти общие операции на самом деле связаны друг с другом. Понимание того, как зависимости загружаются в платформу и добавление новой зависимости в платформу для внедрения, может потребовать прочтения большого количества справочного материала и следования некоторым основным учебникам по платформе. Это может превратить некоторые простые задачи в довольно трудоемкие.
источник
Я следовал совету Марка Симанна из «Инъекции зависимостей в .NET» - чтобы догадаться.
DI следует использовать, когда у вас есть «изменчивая зависимость», например, есть разумная вероятность, что она может измениться.
Поэтому, если вы думаете, что в будущем у вас может быть несколько реализаций или реализация может измениться, используйте DI. В противном случае
new
все в порядке.источник
Моя самая большая любимая мозоль о DI уже упоминалась в нескольких ответах, но я немного подробнее остановлюсь здесь. DI (как это обычно делается сегодня, с контейнерами и т. Д.) Действительно, действительно ухудшает читабельность кода. И читабельность кода, возможно, является причиной большинства современных инноваций в программировании. Как кто-то сказал - писать код легко. Читать код сложно. Но это также чрезвычайно важно, если только вы не пишете какую-то крошечную утилиту для однократной записи.
Проблема с DI в этом отношении в том, что он непрозрачный. Контейнер представляет собой черный ящик. Объекты просто откуда-то появляются, и вы понятия не имеете - кто их создал и когда? Что было передано конструктору? С кем я делюсь этим экземпляром? Кто знает...
А когда вы работаете в основном с интерфейсами, все возможности вашей среды IDE "идут к определению" оказываются в дыму. Очень трудно отследить поток программы, не запуская ее и просто шагая к ней, чтобы увидеть, КАКОЕ внедрение интерфейса было использовано в ЭТОМ конкретном месте. И иногда возникает какое-то техническое препятствие, которое не дает вам пройти. И даже если вы можете, если это связано с прохождением витой кишки контейнера DI, все это быстро превращается в разочарование.
Для эффективной работы с фрагментом кода, который использует DI, вы должны быть знакомы с ним и уже знать, что и где.
источник
Хотя DI в целом, безусловно, хорошая вещь, я бы посоветовал не использовать его вслепую для всего. Например, я никогда не впрыскиваю регистраторы. Одно из преимуществ DI - сделать зависимости явными и понятными. Нет никакого смысла в перечислении
ILogger
как зависимости почти каждого класса - это просто беспорядок. Регистратор несет ответственность за обеспечение необходимой гибкости. Все мои регистраторы являются статическими конечными участниками, я могу рассмотреть возможность внедрения регистратора, когда мне нужен нестатический.Это является недостатком данной структуры DI или структуры насмешки, а не самого DI. В большинстве мест мои занятия зависят от конкретных классов, а это значит, что здесь нет нужды в шаблоне. Guice (платформа Java DI) по умолчанию привязывает класс к себе, и мне нужно только переопределить привязку в тестах (или вместо этого связать их вручную).
Я создаю интерфейсы только при необходимости (что довольно редко). Это означает, что иногда мне приходится заменять все вхождения класса интерфейсом, но среда IDE может сделать это за меня.
Да, но избегайте любых дополнительных шаблонов .
Нет. Будет много классов значений (неизменяемых) и данных (изменяемых), где экземпляры просто создаются и передаются, и где нет смысла их вводить - так как они никогда не сохраняются в другом объекте (или только в другом). такие объекты).
Для них вам может потребоваться ввести фабрику вместо этого, но в большинстве случаев это не имеет смысла (представьте, например, что
@Value class NamedUrl {private final String name; private final URL url;}
вам действительно не нужна фабрика здесь и нечего вводить).ИМХО, это нормально, пока не вызывает раздувания кода. Внедрите зависимость, но не создавайте интерфейс (и без сумасшедшего конфигурационного XML!), Как вы можете сделать это позже без каких-либо хлопот.
На самом деле, в моем текущем проекте есть четыре класса (из сотен), которые я решил исключить из DI, поскольку это простые классы, используемые в слишком многих местах, включая объекты данных.
Другим недостатком большинства платформ DI являются накладные расходы времени выполнения. Это можно перенести во время компиляции (для Java есть Dagger , понятия не имею о других языках).
Еще один недостаток - магия, происходящая повсюду, которую можно отключить (например, я отключил создание прокси при использовании Guice).
источник
Я должен сказать, что, по моему мнению, само понятие внедрения зависимостей переоценено.
DI является современным эквивалентом глобальных ценностей. Внедряемые вами вещи - это глобальные синглтоны и объекты чистого кода, иначе вы не сможете их внедрить. В большинстве случаев использование DI навязано вам для использования данной библиотеки (JPA, Spring Data и т. Д.). По большей части, DI обеспечивает идеальную среду для выращивания и выращивания спагетти.
Честно говоря, самый простой способ проверить класс - убедиться, что все зависимости созданы в методе, который можно переопределить. Затем создайте класс Test, полученный из фактического класса, и переопределите этот метод.
Затем вы создаете экземпляр класса Test и тестируете все его методы. Это не будет понятно некоторым из вас - тестируемые вами методы относятся к тестируемому классу. И все эти тесты методов происходят в одном файле класса - классе модульного тестирования, связанном с тестируемым классом. Здесь нет никаких накладных расходов - так работает модульное тестирование.
В коде эта концепция выглядит следующим образом ...
Результат в точности эквивалентен использованию DI - то есть ClassUnderTest настроен для тестирования.
Разница лишь в том, что этот код очень лаконичен, полностью инкапсулирован, проще в кодировании, легче для понимания, быстрее, использует меньше памяти, не требует альтернативной конфигурации, не требует каких-либо структур, никогда не будет причиной появления 4-х страниц. (WTF!) Трассировка стека, которая включает в себя именно те классы ZERO (0), которые вы написали, и совершенно очевидна для любого, имеющего даже малейшее знание ОО, от новичка до Гуру (вы думаете, но ошибаетесь).
При этом, конечно, мы не можем использовать это - это слишком очевидно и недостаточно модно.
В конце концов, тем не менее, меня больше всего беспокоит DI, потому что проекты, которые я видел, с треском провалились, все они были массивными базами кода, где DI был связующим звеном. DI - это не архитектура, это действительно актуально только в нескольких ситуациях, большинство из которых навязывается вам для использования другой библиотеки (JPA, Spring Data и т. Д.). По большей части, в хорошо разработанной кодовой базе большинство случаев использования DI будет происходить на уровне ниже, чем ваши ежедневные действия по разработке.
источник
bar
могут вызываться только послеfoo
, то это плохой API. Он заявляет о предоставлении функции (bar
), но на самом деле мы не можем его использовать (поскольку,foo
возможно, его не вызывали). Если вы хотите придерживаться своегоinitializeDependencies
(анти?) Паттерна, вы должны по крайней мере сделать его закрытым / защищенным и автоматически вызывать его из конструктора, чтобы API был искренним.На самом деле ваш вопрос сводится к "Является ли юнит-тестирование плохим?"
99% ваших альтернативных классов для инъекций будут mocks, которые позволяют юнит-тестирование.
Если вы проводите модульное тестирование без DI, у вас возникает проблема, как заставить классы использовать поддельные данные или поддельные службы. Скажем «часть логики», так как вы не можете разделить ее на сервисы.
Есть альтернативные способы сделать это, но DI хороший и гибкий. Как только вы включите его для тестирования, вы фактически будете вынуждены использовать его повсюду, так как вам нужен какой-то другой фрагмент кода, даже так называемый «DI для бедного человека», который создает конкретные типы.
Трудно представить себе такой недостаток, чтобы преимущество модульного тестирования было подавлено.
источник
new
внутри метода, мы знаем, что во время тестов выполнялся один и тот же код, работающий в рабочей среде.Order
пример - модульное тестирование: это тестирование метода (total
методаOrder
). Вы жалуетесь, что это код вызова из 2 классов, мой ответ - ну и что ? Мы не тестируем «2 класса одновременно», мы тестируемtotal
метод. Нас не должно волновать, как метод выполняет свою работу: это детали реализации; их тестирование вызывает хрупкость, тесную связь и т. д. Мы заботимся только о поведении метода (возвращаемое значение и побочные эффекты), а не о классах / модулях / регистрах ЦП / и т. д. это используется в процессе.