что такое внедрение зависимостей
Основы внедрения зависимостей
В этой статье я расскажу об основах внедрения зависимостей (англ. Dependency Injection, DI) простым языком, а также расскажу о причинах использования этого подхода. Эта статья предназначена для тех, кто не знает, что такое внедрение зависимостей, или сомневается в необходимости использования этого приёма. Итак, начнём.
Что такое зависимость?
Прежде чем продолжить, я хочу уточнить, что такая взаимосвязь — это хорошо, ведь нам не нужно, чтобы один класс выполнял всю работу в приложении. Нам необходимо разделять логику на разные классы, каждый из которых будет отвечать за определенную функцию. И в таком случае классы смогут эффективно взаимодействовать.
Как работать с зависимостями?
Давайте рассмотрим три способа, которые используются для выполнения задач по внедрению зависимостей:
Первый способ: создавать зависимости в зависимом классе
Проще говоря, мы можем создавать объекты всякий раз, когда они нам нужны. Посмотрите на следующий пример:
Это очень просто! Мы создаем класс, когда нам это необходимо.
Преимущества
Недостатки
Каждый класс должен выполнять лишь свою работу.
Поэтому мы не хотим, чтобы классы отвечали за что-либо, кроме своих собственных задач. Внедрение зависимостей при этом является дополнительной задачей, которую мы ставим перед ними.
Второй способ: внедрять зависимости через пользовательский класс
Итак, понимая, что внедрение зависимостей внутри зависимого класса — не самая лучшая идея, давайте изучим альтернативный способ. Здесь зависимый класс определяет все необходимые ему зависимости внутри конструктора и позволяет пользовательскому классу предоставлять их. Является ли такой способ решением нашей проблемы? Узнаем немного позже.
Посмотрите на пример кода ниже:
Преимущества
Недостатки
Второй способ очевидно работает лучше первого, но у него всё ещё есть свои недостатки. Возможно ли найти более подходящее решение? Прежде чем рассмотреть третий способ, давайте сначала поговорим о самом понятии внедрения зависимостей.
Что такое внедрение зависимостей?
Внедрение зависимостей — это способ обработки зависимостей вне зависимого класса, когда зависимому классу не нужно ничего делать.
Исходя из этого определения, наше первое решение явно не использует идею внедрения зависимостей, а второй способ заключается в том, что зависимый класс ничего не делает для предоставления зависимостей. Но мы все ещё считаем второе решение плохим. ПОЧЕМУ?!
Поскольку определение внедрения зависимости ничего не говорит о том, где должна происходить работа с зависимостями (кроме как вне зависимого класса), разработчик должен выбрать подходящее место для внедрения зависимостей. Как видно из второго примера, пользовательский класс является не совсем правильным местом.
Как же сделать лучше? Давайте рассмотрим третий способ обработки зависимостей.
Третий способ: пусть кто-нибудь ещё обрабатывает зависимости вместо нас
Согласно первому подходу зависимые классы отвечают за получение своих собственных зависимостей, а во втором подходе мы переместили обработку зависимостей из зависимого класса в пользовательский класс. Давайте представим, что существует кто-то другой, кто мог бы обрабатывать зависимости, вследствие чего ни зависимый, ни пользовательский классы не выполняли бы эту работу. Этот способ позволяет работать с зависимостями в приложении напрямую.
«Чистая» реализация внедрения зависимостей (по моему личному мнению)
Ответственность за обработку зависимостей возлагается на третью сторону, поэтому ни одна часть приложения не будет с ними взаимодействовать.
Внедрение зависимостей — это не технология, фреймворк, библиотека или что-то подобное. Это просто идея. Идея работать с зависимостями вне зависимого класса (желательно в специально выделенной части). Вы можете применять данную идею, не используя какие-либо библиотеки или фреймворки. Тем не менее, мы обычно обращаемся к фреймворкам для внедрения зависимостей, потому что это упрощает работу и позволяет избежать написания шаблонного кода.
Любой фреймворк внедрения зависимостей имеет две неотъемлемые характеристики. Вам могут быть доступны и другие дополнительные функции, но эти две функции будут присутствовать всегда:
Во-вторых, фреймворки позволяют определить, как нужно предоставить каждую зависимость, и это происходит в отдельном файле (файлах). Приблизительно это выглядит так (учитывайте, что это лишь пример, и он может отличаться от фреймворка к фреймворку):
Преимущества
Обратите внимание, никакой код внутри приложения не меняется, только метод провайдера. Кажется, что ничего не может быть ещё проще и гибче.
Недостатки
Заключение
В этой статье я попытался объяснить основы работы с понятием внедрения зависимостей, а также перечислил причины необходимости использования этой идеи. Существует ещё множество ресурсов, которые вы можете изучить, чтобы больше узнать о применении DI в ваших собственных приложениях. Например, этой теме посвящён отдельный раздел в продвинутой части нашего курса Android-профессии.
Внедрение зависимостей и принцип инверсии зависимостей это НЕ ОДНО И ТО ЖЕ
Перевод статьи подготовлен в преддверии старта курса «Разработчик C++».
Давным-давно, когда я только начинал вести блог на LosTechies, я написал несколько статей о инверсии зависимостей (Dependency Inversion — DI), внедрении (или инъекции) зависимостей (Dependency Injection — также DI) и, конечно, о том, как я наконец начал понимать, в чем суть внедрения зависимостей. В то время я полагал, что оба DI были просто вариациями названия одной техники — называли ли вы это «инъекция» или «инверсия».
Где-то через год после этих двух публикаций я написал статью для Code Magazine о принципах разработки программного обеспечения SOLID. В процессе написания я попросил моего тогдашнего сотрудника Дерека Грира побыть рецензентом этой статьи. Это оказался самый лучшим поступок, который я когда-либо совершал для моего понимания SOLID, потому что Дерек был достаточно любезен (и терпелив по отношению к моему упрямству), чтобы показать мне, где мое понимание инверсии зависимостей отличалось от правильного. Он нашел время, чтобы провести меня через оригинальные статьи Дяди Боба, объяснить, где я смешивал внедрение зависимостей с инверсией, и наконец разъяснил для меня эту тему. В результате у меня получилась отличная статья, которая до сих пор достаточно популярна — и я очень благодарен Дереку за помощь в исправлении моего понимания.
Впрочем, я и по сей день продолжаю видеть, как другие люди совершают ту же ошибку — даже в ruby. Поэтому, чтобы помочь другим понять, чем является инверсия зависимостей, и чем она не является, я повторно публикую здесь раздел про нее из моей статьи про SOLID. Я понимаю, что часть текста не будет иметь смысла вне контекста остальных частей статьи. Однако общий принцип должен быть очевиден, а вы имеете возможность в любой момент перейти по ссылке на статью, чтобы прояснить интересующие вас моменты.
(Юридическая формальность: следующий текст первоначально появился в выпуске журнала CODE Magazine за январь/февраль 2010 года и воспроизводится здесь с их разрешения)
Принцип инверсии зависимостей
Принцип инверсии зависимостей (Dependency Inversion Principle — DIP) состоит из двух частей:
Тот же принцип применим и в разработке программного обеспечения. Вместо того чтобы работать с набором классов, которые жестко связаны между собой, вы бы хотели работать со стандартным интерфейсом. Более того, вы бы хотели гарантировать, что сможете заменить реализацию, не нарушая ожидания этого интерфейса, согласно LSP (Liskov Substitution Principle — Принцип подстановки Лисков). Таким образом, если вы работаете с интерфейсом и хотите иметь возможность заменять его, вам нужно следить за тем, чтобы вы работали только с интерфейсом, а не с конкретной реализацией. То есть код, который полагается на интерфейс, должен знать подробности только об этом интерфейсе. Он не должен знать ни о каких конкретных классах, которые реализуют этот интерфейс.
Взаимоотношения политик, деталей и абстракций
Немного перефразировав принцип инверсии, можно сказать, что политика (высокий уровень) не должна зависеть от детали (реализации), а деталь должна зависеть от политики. Политика более высокого уровня должна определять абстракцию, к которой она будет обращаться, где некоторая реализация детали выполняет запрошенное действие. Эта перспектива помогает проиллюстрировать, почему это принцип именно инверсии зависимости, а не просто принцип абстракции зависимости.
В качестве примера того, почему зависимость детали от политики является инверсией зависимости, рассмотрим код, который мы написали в FormatReaderService. Служба чтения форматированных файлов — это политика. Она определяет, что интерфейс IFileFormatReader должен делать — ожидаемое поведение этих методов. Это позволяет вам сосредоточиться на самой политике, определяя, как работает служба чтения, без учета реализации деталей — читателей отдельных форматов. Таким образом, читатели зависят от абстракции, предоставляемой службой чтения. В конце концов, и служба и отдельные читатели зависят от абстракции интерфейса читателя формата.
Уменьшение связанности путем инвертирования зависимостей
Вы знаете, что не иметь зависимостей (иметь нулевую связанность) для класса нецелесообразно. У вас не было бы пригодного для использования набора классов, если бы у вас была нулевая связанность. Тем не менее, вы также хотели бы по возможности избавляться от прямой связанности. Вы хотели бы разделить вашу систему настолько, чтобы это позволило менять ее отдельные части, не изменяя ничего, кроме этих самых частей. Принцип инверсии зависимости является ключом к этому замыслу. Опираясь только на абстракцию, такую как интерфейс или базовый класс, вы можете уменьшать связанность между различными частями вашей системы. Это позволяет перекомпоновывать систему различными реализациями.
Рассмотрим множество классов, которые необходимо инстанцировать в четкой иерархии, чтобы вы могли получить некоторую, нужную вам функциональность. Проще всего с помощью класса самого высокого уровня (того, который вы хотите вызывать) создавать экземпляр класса на один уровень ниже, с его же помощью инстанцировать класс еще ниже по уровню, и так далее. На рисунке 14 представлен граф объектов, в котором объект более высокого уровня (политика) зависит и напрямую связан с объектом более низкого уровня (деталью).
Рисунок 14: Политика связанная с деталью.
Это реализует необходимую иерархию, но напрямую связывает классы. Вы не сможете использовать Foo, не таская за собой Bar. Если вы хотите разделить эти классы, вы можете легко ввести интерфейс, который будет реализовывать Bar, и от которого будет зависеть Foo. На рисунке 15 показан простой интерфейс IBar, который вы можете создать на основе public API класса Bar.
Рисунок 15: Разделение с помощью абстракции.
В этом сценарии вы можете отделить реализацию Bar от использования ее в Foo, введя интерфейс. Тем не менее, вы лишь отделили реализацию, выделив из нее интерфейс. Вы еще не инвертировали структуру зависимостей и не исправили все проблемы связанности в этой схеме.
Что же произойдет, когда в данном сценарии вы захотите изменить Bar? В зависимости от того, какие изменения вы хотите внести, вы можете спровоцировать цепную реакцию, которая заставит вас изменить интерфейс IBar. Foo зависит от интерфейса IBar, поэтому вы также должны изменить реализацию Foo. Возможно, вы отделили реализацию Bar, но оставили Foo зависимым от изменений в Bar. То есть Политика по-прежнему зависит от Детали.
Если вы хотите инвертировать структуру зависимостей, чтобы Деталь стала зависимой от Политики, то в первую очередь вы должны поменять свою перспективу. Разработчик, работающий с этой системой, должен понимать, что вы не только абстрагировали реализацию от интерфейса. Да, это разделение необходимо, но этого недостаточно. Вы должны понимать, кому принадлежит абстракция — Политике или Детали.
Принцип инверсии зависимостей гласит, что детали должны зависеть от политик. Это означает, что у вас должна быть политика, определяющая и владеющая абстракцией, которую реализует деталь. В сценарии Foo->IBar->Bar вам нужно рассматривать IBar как часть Foo, а не просто как обертку для Bar. Хоть структурно ничего и не поменялось, но перспектива владения изменилась, как показано на рисунке 16.
Рисунок 16: Политика владеет абстракцией. Деталь зависит от политики.
Если Foo владеет абстракцией IBar, вы можете поместить эти две конструкции в пакет, независимый от Bar. Вы можете поместить их в свое собственное пространство имен, в свою собственную сборку и т. д. Это может значительно увеличить наглядность того, какой класс или модуль зависит от какого. Если вы видите, что AssemblyA содержит Foo и IBar, а AssemblyB обеспечивает реализацию IBar, то вам легче заметить, что детализация Bar зависит от политики, определенной Foo.
Если вы правильно инвертировали структуру зависимостей, цепная реакция на изменения политики и/или деталей теперь также будет корректной. Когда вы меняете реализацию Bar, вы больше не наблюдаете восходящей цепочки изменений. Это связано с тем, что Bar требуется соответствие абстракции, предоставленной Foo — детали больше не диктуют изменения в политике. Затем, когда вы изменяете потребности Foo, вызывая изменения в интерфейсе IBar, ваши изменения распространятся по структуре. Bar (деталь) будет вынужден меняться в зависимости от изменения политики.
Разделение и инверсия зависимостей системы отправки электронной почты
Просматривая нашу кодовую базу, вы можете заметить, что IFileFormatReader уже является примером инверсии зависимости. Класс FormatReaderService владеет определением интерфейса читателя формата. Если потребности службы чтения форматированных файлов изменятся, вы, скорее всего, увидите цепочку изменений нисходящую к читателям форматов. Однако, если изменится читатель конкретного формата файлов, вы вряд ли увидите, что эти изменения отразятся на службе чтения форматированных файлов. Это заставляет задуматься, где еще вы можете инвертировать системные зависимости.
Первое, что вы хотите сделать, это отделить логику получения логовых сообщений и отправки его по электронной почте от формы отправки. Вы не возражаете против ссылок на две службы чтения и отправки электронной почты, но вопрос когда и что вызывать порождает сомнения. Вы осознаете, что процесс фактически дублируется в форме: один раз для отправки из файла и один раз для отправки из базы данных. И затем вы вспоминаете все другие отделы, которые также это используют, и начинаете задаваться вопросом, о реальных масштабах дублирования процесса. Кроме того, некоторые из ваших друзей недавно говорили о «модульном тестировании». Они говорят, что вы должны убедиться, что реальная логика процесса, которую вы тестируете, инкапсулирована в объекты, которые не имеют ссылок на внешние системы.
И с этим всем на уме, вы решаете создать объект с именем ProcessingService. После нескольких минут тасования кода туда-сюда, чтобы попытаться консолидировать процесс, вы понимаете, что не хотите, чтобы служба обработки была связана напрямую со службами чтения базы данных или чтения файлов. После небольшого размышления вы замечаете между ними паттерн: метод «GetMessageBody». Используя этот метод в качестве основы, вы создаете новый интерфейс IMessageInfoRetriever и реализует его как службой чтения базы данных, так и службой чтения файлов.
Этот интерфейс позволяет вам предоставить любую реализацию для служб обработки, которая вам понадобится. Затем вы обращаете внимание на службу электронной почты, которая в настоящее время напрямую связана со службой обработки. Простой интерфейс IEmailService решает эту проблему. На рисунке 17 показана полученная структура.
Рисунок 17: Инвертирование зависимостей службы обработки и службы чтения файлов.
Передача интерфейсов извлечения информации о сообщениях и служб электронной почты в службу обработки гарантирует наличие экземпляра любого класса, реализующего эти интерфейсы, без необходимости знать о конкретных типах экземпляров.
Внедрение зависимостей (dependency injection) через свойства-функции в JavaScript
Известный, но не очень популярный способ внедрения зависимостей. Попытка реализовать этот способ в популярных DI npm пакетах. Еще один свой DI.
Несколько слов об OOP и DI
Тему противопоставления ООП другим парадигмам хотел бы оставить в стороне. На мой взгляд в одном приложении вполне могут сочетаться разные парадигмы. Считаю ES классы большим шагом в сторону привлекательности js для использования ООП.
Небольшая история из личного опыта. В 2006 году был гораздо более популярен, чем сейчас — язык PERL. Он гибкий. Я в том году написал свою OO реализацию, и небольшое приложение, язык PERL это позволяет, пара мануалов 1, 2, и безграничные возможности.
Потом вернулся к этой поделке через 2 месяца, понял что потратил приличное количество времени чтобы погрузиться в свою же ОО реализацию, да и она оказалась не такой уж хорошей, как я думал изначально. Еще через полгода снова пришлось вернуться, и понял что потратил еще больше времени чтобы понять свой же код. Плюс пришлось потратить время на поддержку этой ОО реализации. Для себя сделал вывод, что больше так делать не буду.
Программируя на JavaScript, я чувствовал, что будет такая же проблема. Вроде ООП хочется, но посмотришь вокруг сколько вариантов как это сделать с прототипной моделью, и все не стандарты.
TypeScript тогда не было, но и когда появился с первого раза у меня ничего не получилось, все на каком-то ровном месте пляски с бубном были (это конечно субъективно). Тогда не срослось.
У меня был внутренний настрой, чем меньше JS в проекте, тем лучше. Я использовал JQuery UI Widget Factory. Не идеально, но можно расширять и какой-никакой стандарт, и в целом достаточно быстро получалось. Сейчас ES6 classes после множества локальных реализаций классов на ES5 просто прорыв и возможность использовать ООП. И по появлению ES6 классов можно подумать и о новых реализациях DI.
Внедрение зависимостей (dependency injection) считаю важным инструментом парадигмы ООП. Все легко, когда мы хотим отнаследоваться от одного класса, и немножко изменить поведение под свой проект. Но если мы добавляем сложную библиотеку из нескольких классов, и в ней есть DI, то получаем гибкое приложение.
DI может избавить библиотеку от монструозности. Например, библиотека — календарик. Вариаций, как может быть нарисован календарь бесконечное количество (один/несколько месяцев, формат даты, времени, язык, стандарты. ). Предусмотреть все возможные варианты как аргументы/параметры автору библиотеки просто невозможно. А если и захочет, то простенький календарик может превратиться в “монстрокалендарь”, который будут бояться использовать из-за его размеров. Но если будет возможность конечному клиенту легко чуточек допилить под себя или подключить плагин — календарик становится прекрасным! Вполне себе аргументы для использования DI.
В написании тестов DI может быть полностью самодостаточным инструментом — помощником.
По теме статьи
Один из вариантов реализации внедрения зависимостей — через свойство, причем в свойстве и передается зависимость. Javascript позволяет определять функции как переменные. А прототипная модель позволяет легко менять контексты у этих функций. В итоге можно реализовать внедрение зависимостей через функции, которые возвращают необходимые зависимости.
Проще всего пояснить примером.
Допустим есть класс App приложения,
класс Storage — какое то хранилище (один экземпляр на все приложение singleton/service),
и класс Date, для работы с датой (под каждую дату понадобится отдельный экземпляр).
Функции-свойству которая каждый след. вызов будет создавать новый объект (transient) добавим префикс “new”.
Функции-свойству всегда отдающую один и тот же объект (singleton) добавим префикс “one”.
Мне нравится такой подход, тем что он максимально универсален. Так можно внедрять все что угодно и по умолчанию отложено (lazy). Когда добавляется много вариантов из конкретных реализаций, как внедрять (например в inversify: to, toSelf, toConstantValue, toDynamicValue, toConstructor, toFactory, toFunction, toAutoFactory, toProvider, toService), вся концепция DI становится сложной на ровном месте. Поэтому если внедрять везде одинаково, то можно писать быстрее.
Часто конкретная реализация DI накладывает определенные требования на код самих компонент или сами компоненты становятся зависимы от системы внедрения зависимостей. Я попробовал этот пример оформить в популярных DI реализациях, найти максимально универсальный формат компоненты, заодно оформить их в некую сравнительную табличку. Ниже опишу мои впечатления от различных DI реализаций.
Разные трактовки назначения dependency injection
Прежде, чем привести табличку, хочу обратить внимание на то, что все библиотеки очень разные. И дополнительная разница появляется от разных трактовок назначения dependency injection. Я условно их разделил по своему видению:
Мало где уделяют внимание на независимость компонент от DI. Но на мой взгляд у любой библиотеки появляется дополнительное преимущество, если ее компоненты могут работать с разными DI реализациями, а не тянут конкретную вместе с собой.
Во многих DI реализациях используются декораторы. Можно в библиотеке код компоненты оставить чистым, а декорировать в отдельном файле. Для клиента появляется вариант, либо импортировать чистый компонент, либо вместе с конкретным DI декоратором.
В целом, чем выше цифра, тем больше абстракций, больше гибкость, больше времени на разработку, ниже скорость исполнения кода. Поэтому говорить что, что-то лучше, или что-то хуже, неправильно. Есть разные инструменты для разных нужд. Выбор инструмента соответствующего задаче — настоящее “кунг-фу” )
Популярные dependency injection вспомогательные библиотеки javascript/typescript
Сделал небольшой парсер, разбирающий попадание сочетания “di” в npm. Пакетов по этой теме
1400. Все рассмотреть невозможно. Рассмотрел в порядке уменьшения количества npm dependents.
repo | npm dependents | npm weekly downloads | github stars | возраст, лет | последняя правка, мес назад | lang | ES classes | interfaces | inject property | bundle size, KB | open github issues | github forks |
inversify/Inversifyjs | 1798 | 408k | 6.6k | 6 | 1 | TS | + | + | + | 63.3 | 204 | 458 |
typestack/typedi | 353 | 62k | 1.9k | 5 | 3 | TS | + | + | + | 30.3 | 17 | 98 |
thlorenz/proxyquire | 344 | 426k | 2.6k | 8 | 8 | ES5 | ? | ? | ? | ? | 9 | 116 |
jeffijoe/awilix | 244 | 42k | 1.7k | 5 | 1 | TS | + | — | — | 31.7 | 2 | 92 |
aurelia/dependency-injection | 153 | 13k | 156 | 6 | 2 | TS | + | — | ? | ? | 2 | 68 |
stampit-org/stampit | 170 | 22k | 3k | 8 | 1 | ES5 | ? | ? | ? | ? | 6 | 107 |
microsoft/tsyringe | 149 | 80k | 1.5k | 3 | 1 | TS | + | + | — | 30.4 | 27 | 69 |
boblauer/mock-require | 136 | 160k | 441 | 6 | 1 | ES5 | ? | ? | ? | ? | 4 | 29 |
mgechev/injection-js | 105 | 236k | 928 | 4 | 1 | TS | + | -? | ? | 41.7 | 0 | 48 |
young-steveo/bottlejs | 101 | 16k | 1.2k | 6 | 1 | ES5 + D.TS | -? | — | — | 13.3 | 2 | 63 |
jaredhanson/electrolyte | 33 | 1k | 569 | 7 | 1 | ES5 | — | — | — | ? | 25 | 65 |
zhang740/power-di | 10 | 0.2k | 65 | 4 | 1 | TS | + | + | + | 45.0 | 2 | 69 |
jpex-js/vue-inject | 9 | 0.8k | 174 | 4 | 12 | ES5 | — | — | ? | ? | 3 | 14 |
zazoomauro/node-dependency-injection | 5 | 1k | 123 | 4 | 2 | ES6 + D.TS | + | -? | + | 291.0 | 3 | 17 |
justmoon/constitute | 4 | 8k | 132 | 5 | 60 | ES6 | + | -? | — | 56.2 | 4 | 6 |
owja/ioc | 1 | 2k | 158 | 1 | 3 | TS | + | + | + | 11.3 | 4 | 5 |
kraut-dps/di-box | 1 | 0k | 0 | 0 | 1 | ES6 + D.TS | + | + | + | 11.1 | 0 | 0 |
https://github.com/inversify/InversifyJS
Наверное самый сложный, но и мощный пакет, возможно немного субъективно, потому что пример с ним делал самым первым. После него многие другие казались упрощенными версиями )).Наверное сложно придумать кейс, который бы не рассматривался авторами библиотеки. Монстр)
https://github.com/typestack/typedi
Чувствуется, что библиотека мощная, много разных возможностей. К сожалению, пока не смог разобраться, как я могу в App создать два разных экземпляра Date, с разными аргументами конструктора. Быть может здесь есть опытные его пользователи, которые подскажут?
https://github.com/thlorenz/proxyquire
Позволяет оставить код таким какой он есть, подменять содержимое файлов. В большей степени только для тестов. Сложно назвать DI, но для определенных задач может быть очень подходящим.
https://github.com/jeffijoe/awilix
Не получилось реализовать, возникает ошибка “Symbol(Symbol.toPrimitive)”, как я понял, из-за того что в основе библиотеки Proxy, а у меня один из сервисов наследник от нативного Date класса. Не увидел в примерах использования интерфейсов.
https://github.com/aurelia/dependency-injection
Судя по документации и примерам создана именно с основной целью целью иметь возможность разбивать классы на более мелкие. Является частью фреймворка Aurelia.
https://github.com/stampit-org/stampit
Необычная ОО реализация. Множественное наследование. Не пытался что-то делать.
https://github.com/microsoft/tsyringe
Я не фанат Microsoft, но объективно написать реализацию в их библиотеке у меня получилось быстрее всех остальных. Все умеет, специально выделили что инъекция свойства не реализована и никогда не будет реализована.
https://github.com/boblauer/mock-require
По задумке очень похожа на proxyquire.
https://github.com/mgechev/injection-js
Использовалась в Angular 4. Обширные возможности, конкретно мой пример реализовать не получилось, непонятно как в useFactory передать аргумент.
https://github.com/young-steveo/bottlejs
https://github.com/jaredhanson/electrolyte
Не пытался реализовать. Варианты с ES6 классами пока не реализованы автором.
https://github.com/zhang740/power-di
Много возможностей. Есть специальный код для использования вместе с React. Чрезвычайно маленькая документация. Чтобы разобраться как что-либо сделать приходится смотреть тесты пакета. Не без костылей, но реализовал свой пример.
https://github.com/jpex-js/vue-inject
Специфичный для vue без ES6 классов инструмент. Не рассматривал. В этом фреймворке есть и возможность ипспользовать ES6 classes, и есть функционал provide inject через который можно использовать DI. Библиотека кажется устаревшей.
https://github.com/zazoomauro/node-dependency-injection
Конфигурация зависимостей определяется отдельным YAML/JS/JSON файлом. Для сервера. Основана на концепции фреймворка на php symfony Мой пример сделать не получилось, думал через костыли и передачу класса в setParameter, но и там ограничение, невозможно использовать конструктор класса как параметр.
https://github.com/justmoon/constitute
Реализовал, но костылями, которые аннулируют все DI преимущества.
https://github.com/owja/ioc
Сначала показался новой версией inversify, облегченной и удобной. Возможно это авторешение циклических зависимостей, но кажется неочевидным: чтобы прописать зависимости у компонента, уже на входе надо иметь ссылку на инстанс контейнера, определяющего эти зависимости. По моему мнению с таким подходом связность увеличивается, а не уменьшается.
https://github.com/kraut-dps/di-box
Мой велосипед, подробнее ниже.
Я понимаю что мой пример получился замороченный, и есть много проектов без таких требований, но если DI реализация может больше, это большой плюс на будущее, ведь проекты развиваются.
Свой велосипед
Основан на прототипной “магии”, пример совсем без каких либо библиотек:
Непосредственно в библиотеке несколько инструментов чтобы создавать синглтоны (.one()), не писать bind(this), контролировать заполненность обязательных свойств. С библиотекой этот же пример выглядит так:
Контроль обязательных свойств такой:
Конструкторы.
При написании компонентов для DI реализаций частенько приходится писать много аргументов в конструктор. И через какое то время, приходит мысль, что передача одного объекта со всеми зависимостями удобнее. Передача по ключу, удобнее чем по порядковому номеру.
Но можно пойти еще дальше и попробовать отказаться от конструкторов, не во вред функциональности. Какие задачи у конструктора?
Первый пункт в ES таки подразумевает создание отдельного метода инициализации. Если этого не сделать, то достаточно сложно переопределить конструктор в наследнике из-за этой особенности. А DI изначально задуман для того чтобы сделать компонент более гибким.
Второй пункт можно решить организационным соглашениям. Например все публичные свойства не должны содержать undefined. Можно провести аналогию с абстрактными свойствами и методами из других языков. Как будто все публичные свойства абстрактны.
Если компонент создается в dependency injection реализации, то можно дополнительной проверкой это реализовать. Это поведение по умолчанию библиотеки внедрения зависимостей di-box.
Но для классического подхода или для typescript с удобным синтаксисом типа constructor( public arg1: type, public arg2: type ) это поведение можно убрать опциями при создании Box:
Итого с di-box получаем возможность писать в ООП стиле, с минимальным, но достаточным дополнительным кодом, реализующим DI. С одной стороны в реализации присутствует прототипная “магия”, но с другой она только на мета уровне, и сами компоненты могут быть чистыми, и ничего не знать об окружении.
Буду рад обсуждению, возможно упустил из виду какие-то другие библиотеки. Или может вы знаете как лучше реализовать мой пример в какой-то из реализаций DI. Напишите в комментариях.