Я работаю в проекте, который использует (Spring) Dependency Injection для буквально всего, что является зависимостью класса. Мы находимся в точке, где конфигурационный файл Spring вырос до 4000 строк. Недавно я смотрел один из выступлений дяди Боба на YouTube (к сожалению, я не смог найти ссылку), в котором он рекомендует внедрить только пару центральных зависимостей (например, фабрики, базы данных,…) в основной компонент, из которого они затем будут распределяться.
Преимуществами этого подхода является отделение инфраструктуры DI от большей части приложения, а также он делает конфигурацию Spring более чистой, поскольку фабрики будут содержать гораздо больше того, что было в конфигурации ранее. Напротив, это приведет к распространению логики создания среди многих фабричных классов, и тестирование может стать более трудным.
Поэтому мой вопрос на самом деле заключается в том, какие еще преимущества или недостатки вы видите в том или ином подходе. Есть ли лучшие практики? Большое спасибо за ваши ответы!
источник
Ответы:
Как всегда, это зависит ™. Ответ зависит от проблемы, которую вы пытаетесь решить. В этом ответе я попытаюсь рассмотреть некоторые общие мотивирующие силы:
Порадуйте меньшие базы кода
Если у вас есть 4000 строк кода конфигурации Spring, я полагаю, что база кода имеет тысячи классов.
Это вряд ли проблема, которую вы можете решить после факта, но, как правило, я предпочитаю отдавать предпочтение небольшим приложениям с меньшими базами кода. Если вы находитесь в доменно-управляемом дизайне , вы можете, например, создать базу кода для ограниченного контекста.
Я основываю этот совет на своем ограниченном опыте, поскольку большую часть своей карьеры я писал сетевой бизнес-код. Я мог бы предположить, что если вы разрабатываете настольное приложение, или встроенную систему, или что-то другое, то все сложнее разобрать.
Хотя я и понимаю, что этот первый совет легко наименее практичен, я также считаю, что он самый важный, и поэтому я включаю его. Сложность кода изменяется нелинейно (возможно, экспоненциально) в зависимости от размера базы кода.
Favor Pure DI
Хотя я все еще понимаю, что этот вопрос представляет существующую ситуацию, я рекомендую Pure DI . Не используйте DI-контейнер, но если вы используете, по крайней мере, используйте его для реализации основанной на соглашениях композиции .
У меня нет никакого практического опыта работы со Spring, но я предполагаю, что под конфигурационным файлом подразумевается XML-файл.
Конфигурирование зависимостей с использованием XML является худшим из обоих миров. Во-первых, вы теряете безопасность типов во время компиляции, но ничего не получаете. Файл конфигурации XML может быть настолько же большим, как и код, который он пытается заменить.
По сравнению с проблемой, которую она решает, файлы конфигурации внедрения зависимостей занимают неправильное место на часах сложности конфигурации .
Случай для введения крупнозернистой зависимости
Я могу привести аргументы в пользу крупнозернистой инъекции зависимостей. Я также могу привести довод в пользу мелкозернистого внедрения зависимостей (см. Следующий раздел).
Если вы вводите только несколько «центральных» зависимостей, то большинство классов может выглядеть так:
Это по-прежнему соответствует композиции объектов Design Patterns , а не наследованию классов , поскольку
Foo
создаетBar
. С точки зрения ремонтопригодности это все еще можно считать ремонтопригодным, потому что, если вам нужно изменить состав, вы просто редактируете исходный кодFoo
.Это едва ли менее ремонтопригодно, чем внедрение зависимостей. Фактически, я бы сказал, что проще редактировать класс, который использует
Bar
, вместо того, чтобы следовать косвенному указанию, присущему внедрению зависимости.В первом издании моей книги о внедрении зависимостей я делаю различие между изменчивыми и стабильными зависимостями.
Изменчивые зависимости - это те зависимости, которые вы должны рассмотреть для введения. Они включают
Стабильные зависимости, с другой стороны, являются зависимостями, которые ведут себя четко определенным образом. В некотором смысле, вы могли бы утверждать, что это различие дает основание для грубой инъекции зависимостей, хотя я должен признать, что не полностью осознавал это, когда писал книгу.
Однако с точки зрения тестирования это усложняет юнит-тестирование. Вы больше не можете проводить юнит-тесты
Foo
независимо отBar
. Как объясняет JB Rainsberger , интеграционные тесты страдают от комбинаторного взрыва сложности. Вам буквально придется написать десятки тысяч тестовых случаев, если вы хотите охватить все пути путем интеграции даже 4-5 классов.Контр-аргумент в том, что часто ваша задача не программировать класс. Ваша задача - разработать систему, которая решает некоторые специфические проблемы. Это мотивация развития, ориентированного на поведение (BDD).
Другой взгляд на это представлен DHH, который утверждает, что TDD приводит к повреждению конструкции, вызванному испытаниями . Он также поддерживает грубое интеграционное тестирование.
Если вы возьмете эту точку зрения на разработку программного обеспечения, тогда имеет смысл вводить грубую зависимость.
Случай для введения мелкозернистой зависимости
Тонкодисперсная инъекция зависимостей, с другой стороны, может быть описана как внедрение всех вещей!
Мое основное беспокойство в отношении инъекций грубой зависимости - это критика, высказанная Дж. Б. Райнсбергером. Вы не можете покрыть все пути кода тестами интеграции, потому что вам нужно написать буквально тысячи или десятки тысяч тестовых случаев, чтобы охватить все пути кода.
Сторонники BDD будут противостоять аргументу, что вам не нужно покрывать все пути кода тестами. Вам нужно только покрыть те, которые производят ценность для бизнеса.
Однако, по моему опыту, все «экзотические» пути кода также будут выполняться при развертывании большого объема, и если их не тестировать, многие из них будут иметь дефекты и вызывать исключения во время выполнения (часто исключения с нулевой ссылкой).
Это побудило меня предпочесть детализированное внедрение зависимостей, потому что оно позволяет мне тестировать инварианты всех объектов в изоляции.
Фавор функциональное программирование
Хотя я склоняюсь к детализированному внедрению зависимостей, я сместил акцент на функциональное программирование, в том числе и по другим причинам, потому что оно по сути тестируемо .
Чем больше вы переходите к SOLID-коду, тем более функциональным он становится . Рано или поздно вы можете сделать решающий шаг. Функциональная архитектура - это архитектура портов и адаптеров , а внедрение зависимостей - это тоже попытка портов и адаптеров . Разница, однако, в том, что язык наподобие Haskell обеспечивает реализацию этой архитектуры через систему типов.
Фаворит статически типизированного функционального программирования
На этом этапе я по сути отказался от объектно-ориентированного программирования (ООП), хотя многие проблемы ООП неразрывно связаны с основными языками, такими как Java и C #, больше, чем сама концепция.
Проблема с основными языками ООП состоит в том, что почти невозможно избежать проблемы комбинаторного взрыва, которая, не проверенная, приводит к исключениям во время выполнения. С другой стороны, статически типизированные языки, такие как Haskell и F #, позволяют кодировать многие точки принятия решения в системе типов. Это означает, что вместо того, чтобы писать тысячи тестов, компилятор просто скажет вам, справились ли вы со всеми возможными путями кода (в некоторой степени; это не серебряная пуля).
Кроме того, внедрение зависимости не работает . Истинное функциональное программирование должно отвергать все понятие зависимостей . В результате получается более простой код.
Резюме
Если я вынужден работать с C #, я предпочитаю внедрение детализированных зависимостей, потому что это позволяет мне охватить всю кодовую базу управляемым количеством тестовых случаев.
В конце концов, моя мотивация - быстрая обратная связь. Тем не менее, модульное тестирование - не единственный способ получить обратную связь .
источник
Огромные классы настройки DI являются проблемой. Но подумайте о коде, который он заменяет.
Вам нужно создать экземпляр всех этих сервисов и еще много чего. Код для этого будет либо находиться в
app.main()
начальной точке и вводиться вручную, либо тесно связан, какthis.myService = new MyService();
внутри классов.Уменьшите размер вашего класса установки, разделив его на несколько классов установки и вызывая их с начальной точки программы. то есть.
Вам не нужно ссылаться на service1 или 2, кроме как для передачи их в другие методы настройки ci.
Это лучше, чем пытаться получить их из контейнера, поскольку он обеспечивает порядок вызова настроек.
источник
ci
переменная?