Сколько стоит слишком много инъекций зависимости?

38

Я работаю в проекте, который использует (Spring) Dependency Injection для буквально всего, что является зависимостью класса. Мы находимся в точке, где конфигурационный файл Spring вырос до 4000 строк. Недавно я смотрел один из выступлений дяди Боба на YouTube (к сожалению, я не смог найти ссылку), в котором он рекомендует внедрить только пару центральных зависимостей (например, фабрики, базы данных,…) в основной компонент, из которого они затем будут распределяться.

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

Поэтому мой вопрос на самом деле заключается в том, какие еще преимущества или недостатки вы видите в том или ином подходе. Есть ли лучшие практики? Большое спасибо за ваши ответы!

Antimon
источник
3
Наличие файла конфигурации в 4000 строк не является ошибкой внедрения зависимостей ... есть много способов исправить это: модульный файл в несколько меньших файлов, переключение на внедрение на основе аннотаций, использование файлов JavaConfig вместо xml. По сути, проблема, с которой вы сталкиваетесь, заключается в неспособности управлять сложностью и размером ваших потребностей в внедрении зависимостей.
Возможно_Factor
1
@Maybe_Factor TL; DR разделил проект на более мелкие компоненты.
Вальфрат

Ответы:

44

Как всегда, это зависит ™. Ответ зависит от проблемы, которую вы пытаетесь решить. В этом ответе я попытаюсь рассмотреть некоторые общие мотивирующие силы:

Порадуйте меньшие базы кода

Если у вас есть 4000 строк кода конфигурации Spring, я полагаю, что база кода имеет тысячи классов.

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

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

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

Favor Pure DI

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

У меня нет никакого практического опыта работы со Spring, но я предполагаю, что под конфигурационным файлом подразумевается XML-файл.

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

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

Случай для введения крупнозернистой зависимости

Я могу привести аргументы в пользу крупнозернистой инъекции зависимостей. Я также могу привести довод в пользу мелкозернистого внедрения зависимостей (см. Следующий раздел).

Если вы вводите только несколько «центральных» зависимостей, то большинство классов может выглядеть так:

public class Foo
{
    private readonly Bar bar;

    public Foo()
    {
        this.bar = new Bar();
    }

    // Members go here...
}

Это по-прежнему соответствует композиции объектов Design Patterns , а не наследованию классов , поскольку Fooсоздает Bar. С точки зрения ремонтопригодности это все еще можно считать ремонтопригодным, потому что, если вам нужно изменить состав, вы просто редактируете исходный код Foo.

Это едва ли менее ремонтопригодно, чем внедрение зависимостей. Фактически, я бы сказал, что проще редактировать класс, который использует Bar, вместо того, чтобы следовать косвенному указанию, присущему внедрению зависимости.

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

Изменчивые зависимости - это те зависимости, которые вы должны рассмотреть для введения. Они включают

  • Зависимости, которые должны быть переконфигурированы после компиляции
  • Зависимости, разработанные параллельно другой командой
  • Зависимости с недетерминированным поведением или поведением с побочными эффектами

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

Однако с точки зрения тестирования это усложняет юнит-тестирование. Вы больше не можете проводить юнит-тесты Fooнезависимо от Bar. Как объясняет JB Rainsberger , интеграционные тесты страдают от комбинаторного взрыва сложности. Вам буквально придется написать десятки тысяч тестовых случаев, если вы хотите охватить все пути путем интеграции даже 4-5 классов.

Контр-аргумент в том, что часто ваша задача не программировать класс. Ваша задача - разработать систему, которая решает некоторые специфические проблемы. Это мотивация развития, ориентированного на поведение (BDD).

Другой взгляд на это представлен DHH, который утверждает, что TDD приводит к повреждению конструкции, вызванному испытаниями . Он также поддерживает грубое интеграционное тестирование.

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

Случай для введения мелкозернистой зависимости

Тонкодисперсная инъекция зависимостей, с другой стороны, может быть описана как внедрение всех вещей!

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

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

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

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

Фавор функциональное программирование

Хотя я склоняюсь к детализированному внедрению зависимостей, я сместил акцент на функциональное программирование, в том числе и по другим причинам, потому что оно по сути тестируемо .

Чем больше вы переходите к SOLID-коду, тем более функциональным он становится . Рано или поздно вы можете сделать решающий шаг. Функциональная архитектура - это архитектура портов и адаптеров , а внедрение зависимостей - это тоже попытка портов и адаптеров . Разница, однако, в том, что язык наподобие Haskell обеспечивает реализацию этой архитектуры через систему типов.

Фаворит статически типизированного функционального программирования

На этом этапе я по сути отказался от объектно-ориентированного программирования (ООП), хотя многие проблемы ООП неразрывно связаны с основными языками, такими как Java и C #, больше, чем сама концепция.

Проблема с основными языками ООП состоит в том, что почти невозможно избежать проблемы комбинаторного взрыва, которая, не проверенная, приводит к исключениям во время выполнения. С другой стороны, статически типизированные языки, такие как Haskell и F #, позволяют кодировать многие точки принятия решения в системе типов. Это означает, что вместо того, чтобы писать тысячи тестов, компилятор просто скажет вам, справились ли вы со всеми возможными путями кода (в некоторой степени; это не серебряная пуля).

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

Резюме

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

В конце концов, моя мотивация - быстрая обратная связь. Тем не менее, модульное тестирование - не единственный способ получить обратную связь .

Марк Симанн
источник
1
Мне очень нравится этот ответ, и особенно это утверждение, используемое в контексте Spring: «Настройка зависимостей с использованием XML - худшее из двух миров. Во-первых, вы теряете безопасность типов во время компиляции, но ничего не получаете. Конфигурация XML файл может легко быть таким же большим, как код, который он пытается заменить. "
Томас Карлайл
@ThomasCarlisle - это не та точка конфигурации XML, что вам не нужно касаться кода (даже компилировать), чтобы изменить его? Я никогда (или едва) использовал это из-за двух вещей, упомянутых Марком, но вы действительно получаете что-то взамен.
El Mac
1

Огромные классы настройки DI являются проблемой. Но подумайте о коде, который он заменяет.

Вам нужно создать экземпляр всех этих сервисов и еще много чего. Код для этого будет либо находиться в app.main()начальной точке и вводиться вручную, либо тесно связан, как this.myService = new MyService();внутри классов.

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

main()
{
   var c = new diContainer();
   var service1 = diSetupClass.SetupService1(c);
   var service2 = diSetupClass.SetupService2(c, service1); //if service1 is required by service2
   //etc

   //main logic
}

Вам не нужно ссылаться на service1 или 2, кроме как для передачи их в другие методы настройки ci.

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

Ewan
источник
1
Для тех, кто хочет углубиться в эту концепцию (создание дерева классов в начальной точке программы), лучшим термином для поиска является «составной корень»
e_i_pi
@ Иван, что это за ciпеременная?
Superjos
ваш класс установки ci со статическими методами
Ewan
Ург должен быть ди. я продолжаю думать «контейнер»
Ewan