Каковы недостатки использования инъекций зависимости?

Я пытаюсь представить DI как образец здесь на работе, и один из наших ведущих разработчиков хотел бы знать: Каковы - если таковые имеются - недостатки в использовании шаблона внедрения зависимости?

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


Пояснение: я говорю о паттерне внедрения зависимостей (см. Эту статью Мартина Фаулера), а не о конкретной платформе, будь то на основе XML (например, Spring) или на основе кода (например, Guice), или "самообращаемая",


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

19 ответов

Пара моментов:

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

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

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

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

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

Соответствующие ссылки

http://thedailywtf.com/Articles/The_Inner-Platform_Effect.aspx

http://www.joelonsoftware.com/articles/fog0000000018.html


Вероятно, самая простая форма внедрения зависимости (не смейтесь) - это параметр. Зависимый код зависит от данных, и эти данные вводятся посредством передачи параметра.

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

Давайте возьмем эту простую традиционную функцию - синтаксис C++ здесь не имеет значения, но я должен как-то его прописать...

void Say_Hello_World ()
{
  std::cout << "Hello World" << std::endl;
}

У меня есть зависимость, которую я хочу извлечь и вставить - текст "Hello World". Достаточно просто...

void Say_Something (const char *p_text)
{
  std::cout << p_text << std::endl;
}

Как это более негибко, чем оригинал? Что ж, если я решу, что на выходе должен быть Unicode. Я, вероятно, хочу переключиться с std::cout на std::wcout. Но это означает, что мои строки должны быть wchar_t, а не char. Либо каждый вызывающий объект должен быть изменен, либо (более разумно) старая реализация заменяется адаптером, который переводит строку и вызывает новую реализацию.

Это ремонтные работы, которые не понадобились бы, если бы мы сохранили оригинал.

И если это кажется тривиальным, взгляните на эту реальную функцию из Win32 API...

http://msdn.microsoft.com/en-us/library/ms632680%28v=vs.85%29.aspx

Это 12 "зависимостей" для решения. Например, если разрешение экрана становится действительно огромным, возможно, нам понадобятся 64-битные значения координат - и другая версия CreateWindowEx. И да, уже есть старая версия, которая все еще висит вокруг, которая предположительно отображается за кулисами новой версии...

http://msdn.microsoft.com/en-us/library/ms632679%28v=vs.85%29.aspx

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

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

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

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

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

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

Последние 6 месяцев я активно использую Guice (инфраструктуру Java DI). Хотя в целом я думаю, что это здорово (особенно с точки зрения тестирования), есть определенные недостатки. Наиболее заметно:

  • Код может стать сложнее для понимания. Инъекция зависимости может использоваться очень... творчески... способами. Например, я только что натолкнулся на некоторый код, который использовал пользовательскую аннотацию для внедрения определенных IOStreams (например: @Server1Stream, @Server2Stream). Хотя это работает, и я должен признать, что обладает определенной элегантностью, оно делает понимание инъекций Guice необходимым условием для понимания кода.
  • Более высокая кривая обучения при изучении проекта. Это относится к пункту 1. Чтобы понять, как работает проект, использующий внедрение зависимостей, необходимо понимать как шаблон внедрения зависимостей, так и конкретную среду. Когда я приступил к своей нынешней работе, я провел довольно много смущенных часов, пытаясь понять, что делает Guice за кулисами.
  • Конструкторы становятся большими. Хотя это может быть в значительной степени решено с помощью конструктора по умолчанию или фабрики.
  • Ошибки могут быть запутаны. Моим последним примером этого было столкновение с двумя именами флагов. Guice молча проглотил ошибку, и один из моих флагов не был инициализирован.
  • Ошибки передаются во время выполнения. Если вы неправильно настроили свой модуль Guice (циклическая ссылка, плохая привязка,...), большинство ошибок не будут обнаружены во время компиляции. Вместо этого ошибки выставляются, когда программа фактически запускается.

Теперь, когда я пожаловался. Позвольте мне сказать, что я буду продолжать (охотно) использовать Guice в моем текущем проекте и, скорее всего, в следующем. Внедрение зависимостей - это отличный и невероятно мощный паттерн. Но это определенно может сбить с толку, и вы почти наверняка потратите некоторое время на проклятие в любой среде внедрения зависимостей, которую вы выберете.

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

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

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

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

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

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

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


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

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

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

DI решает эту проблему? Нет Но это помогает вам ясно видеть, пытаетесь ли вы делегировать ответственность за проектирование каждой комнаты ее обитателям.

Иллюзия того, что вы развязали свой код, просто внедрив зависимость без АКТУАЛЬНОЙ развязки. Я думаю, что это самая опасная вещь в DI.

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

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

Public Class MyClass
    Private ReadOnly mExLogHandlerService As IExceptionLogHandlerService

    Public Sub New(exLogHandlerService As IExceptionLogHandlerService)
        Me.mExLogHandlerService = exLogHandlerService
    End Sub

     ...
End Class

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

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

Кстати, некоторые методы могут смягчить эту проблему, позволяя ленивую загрузку внедренных зависимостей, например, предоставляя класс Lazy<IService> экземпляр как зависимость. Это изменило бы конструкторы ваших зависимых объектов и сделало бы их еще более осведомленными о деталях реализации, таких как затраты на создание объектов, что, вероятно, также нежелательно.

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

В частности, если вы нажмете Control-Click/Command-Click при вызове метода в коде, вы перейдете к объявлению метода в интерфейсе, а не в конкретной реализации.

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

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

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

Внедрение зависимостей наоснове конструктора (без помощи магических "структур") - это чистый и полезный способ структурировать ОО-код. В лучших кодовых базах, которые я видел, за годы, проведенные с другими бывшими коллегами Мартина Фаулера, я начал замечать, что большинство хороших классов, написанных таким образом, в конечном итоге имеют один doSomething метод.

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

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

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

(Как заметка, я считаю, что вся тема довольно "религиозная", но ваш пробег будет зависеть от уровня технического фанатизма в вашей команде разработчиков!)

Если вы используете DI без контейнера IOC, самым большим недостатком является то, что вы быстро видите, сколько зависимостей на самом деле имеет ваш код и насколько тесно все на самом деле. ("Но я подумал, что это хороший дизайн!") Естественное продвижение вперед - переход к контейнеру IOC, который может занять немного времени для изучения и реализации (не так плохо, как кривая обучения WPF, но это не бесплатно. или). Последним недостатком является то, что некоторые разработчики начнут писать честные и добрые юнит-тесты, и им потребуется время, чтобы понять это. Разработчики, которые раньше могли бы что-то выяснить за полдня, вдруг потратят два дня, пытаясь понять, как высмеивать все их зависимости.

Как и в случае с ответом Марка Симанна, суть в том, что вы тратите время на то, чтобы стать лучшим разработчиком, а не взламывать куски кода вместе и бросать его за дверь / в производство. Что бы ваш бизнес предпочел? Только ты можешь ответить на это.

Читаемость кода. Вы не сможете легко понять поток кода, так как зависимости скрыты в файлах XML.

DI - это техника или шаблон, не связанные с какой-либо структурой. Вы можете подключить свои зависимости вручную. DI помогает вам с SR (Единая ответственность) и SoC (разделение интересов). DI приводит к лучшему дизайну. С моей точки зрения и опыта нет минусов. Как и с любым другим шаблоном, вы можете ошибиться или использовать его неправильно (но в случае DI это довольно сложно).

Если вы вводите DI в качестве принципа для унаследованного приложения с использованием фреймворка - самая большая ошибка, которую вы можете сделать, - это неправильно использовать его как Service-Locater. DI+Framework сам по себе великолепен и только улучшал его везде, где я его видел! С организационной точки зрения, есть общие проблемы с каждым новым процессом, техникой, шаблоном,...:

  • Вы должны тренировать свою команду
  • Вы должны изменить свою заявку (включая риски)

В общем, вы должны инвестировать время и деньги, кроме того, нет никаких минусов, на самом деле!

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

Две вещи:

  • Им требуется дополнительная поддержка инструмента для проверки правильности конфигурации.

Например, IntelliJ (коммерческая версия) поддерживает проверку правильности конфигурации Spring и будет отмечать ошибки, такие как нарушения типов в конфигурации. Без поддержки такого рода вы не сможете проверить правильность конфигурации перед запуском тестов.

Это одна из причин, почему шаблон "торт" (как он известен сообществу Scala) является хорошей идеей: проводку между компонентами можно проверить с помощью средства проверки типов. У вас нет такой выгоды с аннотациями или XML.

  • Это делает глобальный статический анализ программ очень сложным.

Такие фреймворки, как Spring или Guice, затрудняют статически определить, как будет выглядеть граф объектов, созданный контейнером. Хотя они создают граф объектов при запуске контейнера, они не предоставляют полезных API, которые описывают граф объектов, который / будет / создан.

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

Другие вопросы по тегам