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

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

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

Восстание DI в Java Enterprise Edition

Еще несколько лет назад значительная часть Java (корпоративного) сообщества продвигала Java как лучший выбор для разработки корпоративных приложений. Такие производители, как IBM, Oracle, Sun (теперь Oracle), BEA (теперь также Oracle) и JBoss (RedHat) продали серверы приложений для размещения программного обеспечения, написанного для Java Enterprise Edition (JEE) спецификации и интерфейсы , обеспечивая ценность, несмотря на то, что эти серверы снабжены множеством функций, таких как включение тонкой настройки в реальном времени, мониторинг и специфические особенности сервера приложений (например, кластеризация и эксклюзивные библиотеки), а также безграничная масштабируемость, пока они работают. И чтобы сделать Java привлекательной для программистов с широким спектром навыков (или, я бы сказал, с ограничениями), они позаботились о том, чтобы спецификации были совместимы с IDE, чтобы вы могли заменить код интерфейсами и мастерами перетаскивания. Концепция, в которой тогда шла разработка программного обеспечения, состояла из языков 4-го поколения и коммерциализации программных компонентов высокого уровня (например, проект IBM в Сан-Франциско), который в конечном итоге заменит программистов бизнес-аналитиками.

К сожалению, писать программы по этим стандартам было просто ужасно. Для реализации Hello World вам понадобится примерно 1244 интерфейса, 2488 классов реализации и 4302 файла конфигурации, и тогда это, вероятно, считалось срезанием углов и не совсем соответствовало стандартам Enterprise. Затем код нужно было трижды обернуть в zip-файлы с причудливыми расширениями и файлы метаданных, упакованные и развернутые через конвейер, который, помимо множества нажатий на неуклюжие веб-интерфейсы, также включал прыжки через пылающие обручи и маркировку и повторную упаковку ваших пакетов с помощью Международный комитет архитекторов-астронавтов. Удивительно, что за эти годы на Java вообще что-то построили.

А потом появились Весенние рамки. Spring был умным бунтом против JEE, предлагая альтернативную контейнерную модель, которая позволяла пользователям использовать простые объекты, а не EJB, и в целом предоставляла гораздо более простую модель программирования. Он начинался как сопутствующий код с книгой о дизайне JEE и содержал достаточно глупых вещей, таких как настройка графа объекта с помощью XML и требование интерфейсов абсолютно для всего, - чтобы не попадать в поле зрения но масштабируется для предприятия? толпа людей. Но на самом деле он был сосредоточен на расширении возможностей разработчиков; давая им возможности вместо ограничений и избегая того, что разработчикам не нужно было держать за руку. Как только разработчики это поняли, фреймворк Spring стремительно занял доминирующее положение, которое он занимает до сих пор.

Хотя наиболее важным коммерческим аргументом Spring, вероятно, было то, что это не был JEE, как обычно, внедрение зависимостей было его ключевым элементом. Spring в основном поместил DI на карту, и поскольку Spring с тех пор является самой популярной средой Java, люди в стране Java просто используют внедрение зависимостей в качестве способа по умолчанию для связывания объектов вместе.

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

Голливуд и степени магии

Перед внедрением зависимостей была инверсия управления (IoC). Если вы сейчас прочтете эти термины, IoC позиционируется как общий термин, под которым подпадает DI, но я помню DI как в основном ребрендинг IoC. Тем не менее, в ранних статьях об IoC / D я упоминал голливудский принципал или: не звони нам, мы позвоним тебе. Идея заключается в том, что если вы пишете свои классы в соответствии с определенным интерфейсом, вы должны иметь возможность полагаться на его клиентов для правильного создания и инициализации экземпляров, что означает, что этим классам нужно очень мало знаний о среде, в которой они работают, который четко согласуется с такими целями, как принцип единой ответственности и инкапсуляция.

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

Но как?

Когда дело доходит до того, как обычно реализуется DI, есть два основных варианта: внедрение свойства (установщика или члена) и внедрение конструктора.

Простой DI на основе свойств выглядит так:

public class WindowWasher
{
  public SoapBottle SoapBottle { set; private get; }
}

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

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

Зависимость интерфейса пытается улучшить отсутствие ясности этого шаблона:

public interface IWindowWasher
{
  SoapBottle SoapBottle { set; }
}
public class WindowWasher : IWindowWasher
{
  public SoapBottle SoapBottle { set; private get; }
}

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

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

И, наконец, внедрение конструктора:

public class WindowWasher
{
  readonly SoapBottle soapBottle;
  public WindowWasher(SoapBottle soapBottle)
  {
    this.soap = soap;
  }
}

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

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

Когда дело доходит до поддержки контейнеров, нужно обсудить несколько вариантов.

Нет контейнера

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

Например:

Scent scent = new Scent("flav", "flov");
bool isSoft = true;
Soap soap = new Soap(scent, isSoft);
SoapBottle soapBottle = new SoapBottle(soap);
WindowWasher washer = new WindowWasher(soapBottle);
washer.WipeItGood();

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

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

Требуется явная конфигурация

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

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

На основе соглашений / метаданных

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

Мой любимый фреймворк в мире Java - Guice, который обеспечивает хорошее сочетание явной конфигурации в коде с некоторой эвристикой. Он будет пытаться разрешить экземпляры лениво (например, при ссылке на класс он попытается создать экземпляр и выбрать либо конструктор без аргументов, либо единственный конструктор, помеченный @Inject), даже если этот класс никогда не настраивается явно . И еще одна замечательная функция imho (хотя и не всеми любимая) заключается в том, что вы можете аннотировать интерфейсы, чтобы ссылаться на их реализацию по умолчанию, а затем переопределять это в каждом конкретном случае с помощью явной конфигурации, если вам это нужно.

Код, сравнимый с Guice, выглядел бы так:

[ImplementedBy(WindowWasher)]
public interface IWindowWasher
{
  void WipeItGood();
}
public class WindowWasher : IWindowWasher
{
  readonly SoapBottle soapBottle;
  [Inject]
  public WindowWasher(SoapBottle soapBottle)
  {
    this.soapBottle = soapBottle;
  }
  void WipeItGood() { ... }
}

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

Что не нравится?

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

  • Используйте слишком много магии. Хорошо, когда фреймворк пытается вам помочь. Но когда это происходит, трассировки стека должны оставаться небольшими, правила разрешения простыми и предсказуемыми и не должны приводить к увеличению времени загрузки более чем на несколько секунд. А когда это не удается, отчеты об ошибках должны помочь быстро определить, где что-то пошло не так.
  • Используйте слишком мало магии. Итак, вместо того, чтобы писать здесь и там какой-то код для создания экземпляров ваших классов, когда это необходимо, вы теперь застряли с гигантским объемом кода, «регистрирующим» все возможные экземпляры заранее? Или, что еще хуже, вы должны объявить все это в гигантском XML-файле? Многие разработчики скажут: «Нет, спасибо», и я их не виню!

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

Не убежден?

Давайте кратко рассмотрим альтернативы внедрению зависимостей.

  1. просто создавайте зависимости сами
public class WindowWasher
{
  SoapBottle soapBottle;
  public WindowWasher()
  {
    Scent scent = new Scent("flav", "flov");
    bool isSoft = true;
    Soap soap = new Soap(scent, isSoft);
    soapBottle = new SoapBottle(soap);
  }
  ...
}

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

2. Более разумной реализацией является использование статических фабрик.

public class SoapBottleFactory
{
  static SoapBottle { get; set; }
}

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

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

В принципе, использование статического фабричного шаблона может работать нормально, но зависит от дисциплины программистов, использующих его. И хрен другие программисты :-)

3. Наконец, есть Локатор услуг.

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

Если вы получаете экземпляр в локатор статически, это не сильно отличается от статического шаблона фабрики со всеми потенциальными проблемами, связанными с этим. Это кажется общепринятым пониманием паттерна и обычно не приветствуется.

Заключение

Внедрение зависимостей - полезный шаблон для некоторых языков, таких как Java и C #. В других языках есть разные решения для сборки сервисов, которые могут несколько напоминать DI или полностью отличаться друг от друга, но их объединяет (вероятно) то, что хорошие реализации четко определяют, чего они ожидают от своей среды для функционирования, и сохраняют предположения о среде. код работает до минимума.

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