Вот именно такой вопрос был задан в первом же комментарии к статье "DI-контейнер на чистом JavaScript без TypeScript и reflect-metadata" уважаемого @alexander_kubarski. Я когда-то и сам на Хабре отвечал на этот вопрос ("Зачем нужно внедрение зависимостей в JS"), и уважаемый @Wroud тоже ("Dependency Injection в JavaScript: зачем он вам нужен"), но непонимание места и значения DI у JS-разработчиков всё равно присутствует. Попробую ответить на этот вопрос ещё раз:
Внедрение зависимостей помогает снизить стоимость верификации сложного приложения. Оно отделяет компонент от выбора конкретного окружения и позволяет проверять его относительно явных ожиданий, не поднимая каждый раз весь граф реальных зависимостей.
Именно для этого используют Inversion of Control в целом и Dependency Injection в частности - во всех языках программирования, а не только в JavaScript.
Вопрос, однако, был задан не про DI, а про DI-контейнер. Поэтому сначала придётся разобраться, какую проблему решает само внедрение зависимостей, а затем вернуться к тому, зачем поверх него нужен контейнер.
Когда зависимость становится проблемой
Рассмотрим простую функцию:
import {save} from "./storage.mjs"; export async function saveName(name) { const value = name.trim().toLowerCase(); await save(value); return value; }
Она нормализует имя и сохраняет результат.
Проверить возвращаемое значение недостаточно. Внутри легко допустить ошибку:
await save(name);
Функция по-прежнему вернёт "alex", но в хранилище попадёт исходная строка " Alex ".
Чтобы проверить компонент полностью, нужно поднять конкретное хранилище и убедиться, что оно получило правильное значение.
Пока хранилище представляет собой массив в памяти, это почти ничего не стоит. Но реальное хранилище может зависеть от базы данных, файловой системы, конфигурации, сети и логгера.
Тогда проверка одной небольшой функции постепенно превращается в проверку связанного графа компонентов.
Сам код saveName() не стал сложнее. Подорожало окружение, необходимое для доказательства его корректности.
Что меняет внедрение зависимостей
Уберём из функции выбор конкретного хранилища:
export function createSaveName({storage}) { return async function saveName(name) { const value = name.trim().toLowerCase(); await storage.save(value); return value; }; }
Зависимость не исчезла. Функции по-прежнему требуется хранилище.
Изменилось только место, где выбирается его имплементация.
В работающем приложении мы передадим настоящее хранилище:
const saveName = createSaveName({ storage: databaseStorage, });
В тесте - минимальный объект, достаточный для проверки поведения компонента:
const saved = []; const saveName = createSaveName({ storage: { async save(value) { saved.push(value); }, }, });
Теперь тест проверяет только ответственность saveName():
имя нормализовано; результат передан в storage.save(); возвращено правильное значение.
База данных и остальные зависимости хранилища в этой проверке не участвуют.
Статический import фиксирует конкретную связь внутри компонента. DI переносит выбор имплементации в точку сборки приложения.
Где возникает интерфейс
В JavaScript нет обязательной конструкции interface, но ожидания между компонентами всё равно существуют.
В выражении
await storage.save(value);
уже зафиксировано, что зависимость должна предоставить метод save(), принять значение и позволить дождаться завершения операции.
Это и есть интерфейс связи с точки зрения потребителя.
Интерфейс - это описание ожиданий компонента на границе с его зависимостью.
Такой интерфейс можно зафиксировать через JSDoc, TypeScript, документацию, статический анализ и тесты. Конкретный способ вторичен.
Важно другое: после появления явной границы верификацию можно разделить.
Потребитель проверяется относительно интерфейса.
Каждая реальная имплементация отдельно проверяется на соответствие тому же контракту.
Интеграционные тесты при этом не исчезают. Ошибки могут возникать в конкретных сочетаниях базы данных, драйвера и конфигурации. Но проверять можно наиболее значимые связки, а не механически поднимать весь граф для каждого локального теста.
При достаточно полных контрактах значительная часть стоимости проверки начинает расти ближе к сумме числа компонентов и имплементаций, а не к произведению всех возможных конфигураций.
Зачем здесь контейнер
До сих пор DI-контейнер нам не требовался.
Зависимости можно связывать вручную:
const logger = createLogger(config); const database = createDatabase(config, logger); const storage = createStorage(database, logger); const service = createService(storage, logger); const controller = createController(service, logger);
В небольшом приложении такая сборка обычно прозрачнее любого контейнера.
Проблема возникает позже, когда приложение состоит из большого числа относительно независимых компонентов. Их разрабатывают разные команды, отдельные разработчики или ИИ-агенты. Каждый компонент добавляет свои зависимости и правила сборки.
Центральный bootstrap начинает разрастаться. Для подключения нового модуля разработчику приходится понимать не только свой компонент, но и значительную часть общей композиции приложения.
Сложность становится уже не только технической, но и организационной.
DI-контейнер превращает композицию системы в общий протокол.
Модуль объявляет, какие зависимости ему нужны и какие имплементации он предоставляет. Контейнер собирает из этих правил рабочий граф.
Разработчику одного модуля больше не обязательно понимать устройство всей системы. Ему достаточно соблюдать соглашения интеграции.
Это особенно важно для ИИ-агентов. Агент надёжнее изменяет локальный модуль по известным правилам, чем редактирует вручную центральную точку сборки, для понимания которой требуется глобальный контекст проекта.
DI снижает связанность компонента с конкретным окружением. DI-контейнер снижает зависимость разработчика от знания всей системы.
Контейнер не создаёт архитектурные границы и не делает компоненты автоматически независимыми. Если зависимости скрыты, интерфейсы не определены, а правила связывания случайны, контейнер лишь автоматизирует создание плохо устроенного графа.
Сначала появляются компоненты, явные зависимости и контракты. Затем может понадобиться инструмент, который обслуживает их композицию.
Когда контейнер не нужен
Размер проекта сам по себе ещё не является достаточным критерием.
Контейнер не нужен, если граф зависимостей остаётся небольшим, понятным и стабильно собирается вручную.
Он также не нужен для чистых функций, локальных преобразований и небольших утилит, статический импорт которых не мешает независимой проверке.
Даже наличие нескольких имплементаций ещё не требует контейнера. Их вполне можно выбирать вручную в одной точке композиции.
Контейнер становится полезен, когда сходятся несколько факторов:
много относительно независимых модулей; сложный или изменчивый граф зависимостей; несколько конфигураций приложения; параллельная работа разных разработчиков или агентов; ручная композиция требует слишком много глобального знания.
Граница проходит по стоимости сборки и интеграции системы.
Ответ
JavaScript позволяет подменить почти любой объект во время выполнения. Но сама возможность подмены ещё не создаёт устойчивой архитектурной границы.
DI делает зависимость явной и переносит выбор имплементации за пределы компонента. Это позволяет отдельно проверять потребителей и реализации и тем самым удерживать стоимость верификации под контролем.
DI-контейнер решает следующую проблему: автоматизирует композицию большого числа таких компонентов.
Поэтому точный ответ на вопрос из заголовка звучит так:
DI-контейнер нужен JavaScript-приложению тогда, когда внедрение зависимостей уже помогает разделять и независимо проверять компоненты, а ручная сборка их графа становится самостоятельным источником технической и организационной сложности.
В небольшом приложении контейнер, скорее всего, будет лишним.
В крупной модульной системе, которую параллельно развивают относительно независимые разработчики или ИИ-агенты, он становится общим механизмом интеграции.
P.S.
Данный пост является сокращённой, но достаточной выжимкой более подробной версии разбора с моего сайта. Если кто-то предпочитает TL;DR - wellcome, как говорится.
Комментарии (46)

Oeaoo
21.07.2026 21:50DI делает зависимость явной и переносит выбор имплементации за пределы компонента. Это позволяет отдельно проверять потребителей и реализации и тем самым удерживать стоимость верификации под контролем.
Это аргумент про фабрику, скорее. А фабрика, в свою очередь, является реализацией DI на языке, в сочетании с замыканиями. И без всяких фреймворков. Про "удержание стоимости верификации под контролем" было вообще больно читать.

SolidSnack
21.07.2026 21:50Ага, учитывая что тип можно указать в сигнатуре функции и зависимость тоже будет явная, без всяких DI...
DI это про контейнер который содержит объекты для инъекции, а не про инверсию зависимостей...

flancer Автор
21.07.2026 21:50А фабрика, в свою очередь, является реализацией DI на языке, в сочетании с замыканиями. И без всяких фреймворков.
Можно и так - вручную. Фреймворки лишь помогают унифицировать общие подходы в проекте. Или в проектах. Или в отдельной экосистеме. Всё зависит от масштаба.
Про "удержание стоимости верификации под контролем" было вообще больно читать.
Под стоимостью верификации я имею в виду рост числа возможных композиций. Если у компонента несколько вариативных зависимостей, пространство сборок образуется как декартово произведение их реализаций и растёт мультипликативно.
Интерфейсы позволяют проверить компонент относительно контрактов (интерфейсов), а реализации - отдельно относительно этих контрактов. Поэтому вместо независимой проверки всех сочетаний получаем приблизительно аддитивную проверку частей плюс ограниченное число интеграционных тестов конкретных сборок.
"Программирование - безнадежно проигранная борьба с непреодолимой сложностью кода и вероломством требований" (с) Jonathan Edwards
Согласен, что фраза "удержание чего бы то ни было под контролем" в программировании звучит излишне оптимистично. DI помогает проиграть эту борьбу чуточку позже.

sargas
21.07.2026 21:50это всё спокойно делается без функций обёрток, очередная вода на тему психических отклонений

Balek
21.07.2026 21:50Я к предыдущей статье написал комментарий, но хабр не прислал мне ваш ответ, поэтому извиняюсь, что пропал из переписки.
Эта статья как будто должна отвечать на мой вопрос, но в очередной раз приводит одну единственную проблему, решаемую с помощью DI - тестирование. Поэтому в очередной раз должен задать стандартный вопрос: чем вам не угодил манкипатчинг для тестирования? Используйте мокинг, чтобы подменять ./storage.js и не придётся усложнять продуктовый код.

flancer Автор
21.07.2026 21:50Используйте мокинг, чтобы подменять ./storage.js и не придётся усложнять продуктовый код.
Покажите, пожалуйста, код, как это делается. Я пришёл в JS из мира Java & PHP. Возможно, я просто не знаю каких-то базовых основ или знаю их по-другому.
в очередной раз приводит одну единственную проблему, решаемую с помощью DI - тестирование
Я использую интерфейсы в своём JS-коде без транспиляции из TS (или любого другого ЯП) путём подмены токена интерфейса токеном имплементации в настройках DI-контейнера (препроцессор). Как вы используете интерфейсы в JS?
Я использую Proxy для добавления cross-cutting функциональности в свои приложения на JS. Через post-процессор я могу обернуть любой объект, который используется в качестве зависимости, включая библиотеки nodejs. Как вы это делаете в JS-коде без транспиляции из TS (или любого другого ЯП)?
alhimik45
DI-контейнер очень хорош когда тривиальные случаи разрешаются автоматически на основе типов, как в C# например. В динамическом JS или TS в котором нет нормальной рантайм рефлексии - всё равно выглядит как будто бойлерплейта больше получается с этим ручным менеджментов токенов
flancer Автор
"Ручной менеджмент токенов" - это лишь один из возможных вариантов управления зависимостями. Я, например, и мои агенты, пишем так (ну, или примерно так):
При отсутствующей runtime-рефлексии в JS приходится "колхозить" runtime-рефлексию самому. Зависимости всех доступных снаружи экспортов отдельного es6-модуля прописываются в специальном экспорте
__deps__. После этого анализ зависимостей модуля можно автоматизировать полностью. Синтаксис "языка токенов" может быть любым, я взял для себя namespace'ы из PHP Zend1 фреймворка. Главное, чтобы этот "язык" упаковывал в токене:адрес npm-пакета и es6-модуля в нём;
имя экспорта в es6-модуле;
форму существования зависимости (singleton or transient).
После этого достаточно в настройке контейнера в Composition Root привязать физическое расположение пакета (в локальной файловой системе или в вебе) к соответствующему namespace'у (
App_илиApp_User_) и разрешение зависимостей из__deps__происходит автоматически, как в C#, Java, PHP, ...Решение с
__deps__, кстати, мне ИИ подсказал. Это логика "силиконовых", а не "кожаных". Им так норм.nihil-pro
Это очень сомнительное решение, с какой бы стороны я не пытался на него смотреть
flancer Автор
Что именно вас смущает в этом коде?
Фабрика создаёт объект и инжектит в него зависимость. Обычный JS-код. Можете переписать его под классы, если вам больше нравятся классы.
SolidSnack
Странно что вас ничего не смущает.
Откуда у вас уверенность что repository имеет функцию findNameById, которую вы вызываете в методе?
flancer Автор
А если нет, то - что?
Компонент ожидает, что "repository имеет функцию findNameById" - это его право. В данном случае ожидания выражены через код. Можете добавить JSDoc'и, если хотите подробностей. Мы сейчас про JS говорим, тут - вот так. В других языках контракты (ожидания) описываются по-другому.
SolidSnack
Получается и выкинуть ошибку, а вместе с ней и завалить все приложение, из-за не существуешго метода, тоже право функции?
Реально в js нет интерфейсов... ну это надо как-то проверять тогда в коде чтоли...
мда, жесть конечно, язык как будто для маленьких манипуляций с html сделали, а не для серьёзных проектов...
flancer Автор
Да, конечно. Это в любом ЯП так будет. Если вы в runtime умудритесь подсунуть зависимость не с тем контрактом, завалится всё приложение, если не стоит обработки ошибок. Другое дело, что многие ЯП заставляют разрабов описывать контракты и проверяют соответствие контрактов на этапе компиляции - это уменьшает вероятность получить нечто неожиданное в runtime.
Проверять не надо - неэффективно. Достаточно обеспечить поставку зависимости с соответствующим контрактом и обработку ошибок на нужном уровне приложения.
Так оно и было. Всё остальное наросло сильно позже.
Я начинал программировать на PHP, когда там ещё не было возможности указывать типы в сигнатуре функции (до PHP 5.0, 2004 год):
Очень похоже на нынешний JS, не правда ли?
После 5.0 стало можно писать так:
А после PHP 7.0 (2015) так:
Я уверен, что если бы не Microsoft с TypeScript, то и в JS уже давно была бы такая возможность. Но имеем, что имеем. В JS в 2026-м году нет ни интерфейсов, ни возможности указать типы в сигнатуре функций.
Поэтому вот - описываем контракты в коде. Как в PHP четвертьвековой давности.
SolidSnack
Я вам и пытаюсь сказать что нету никакого контракта у вас.
Получается что ваши репозитории должны знать тело каждой функции и что в ней может быть вызванно. Ладно один пример, учебный, но на реальном проекте это сразу превратится в барьер
Даже наследование класса и переопределение метода выглядит лучше чем "просто знать" где там что вызывается
flancer Автор
Все репозитории должны знать контракт, который ожидается любой функцией, использующей эти репозитории. Можно размазать контракт по множеству всех функций проекта, а можно вынести контракт в отдельный документ и по нему сверять - как ожидания функций-потребителей, так и возможности имплементаций.
В TS есть возможность описать
interfaceв отдельном исходнике, который при транспиляции не превращается в js-код, из-за того, что в JS нет интерфейсов вообще.Зато в JS есть JSDoc и возможность описать интерфейс как в отдельном файле, так и в
types.d.ts(если в пакете интерфейсов не много).Я в примере обращал ваше внимание, что интерфейс зарождается в коде потребителя. Это справедливо для любого ЯП.
Возможно это ставит с ног на голову привычные представления об интерфейсах, но если пристально подумать...
А так вам просто нужно место (файл), где описать какой-то интерфейс в том или ином виде, предусмотренным соответствующим ЯП, или в md- или txt-файле, если этот ЯП ещё менее развит, чем JS с его JSDoc.
SolidSnack
В языке где есть интерфейс достаточно написать implements (например php) и быть уверенным что ваши репозитории поддерживают контракт.
Получается вы как веб разработчик считаете что для домена (бизнес логики) интерфейс задают потребители бизнес логики? Интересно как.
Поймите, для меня как программиста это просто не приемлемо. Сидеть и сверять программу с текстом, когда за выполнением контрактов должен отвечать компилятор/интерпретатор это явно два шага назад
flancer Автор
Да. Но это в тех языках, где "интерфейс" поддерживается на уровне языка. В JS такого нет.
Именно так. Вы исходите из нужд конечного пользователя приложения (потребителя) и выстраиваете весь домен вокруг этих нужд, пытаясь их "угадать" / "вычислить" / "опросить". В коде точно так же. Успешность вашего интерфейса зависит от того, насколько точно вы представляете нужды вашего потребителя.
Полностью согласен с такой постановкой вопроса. Не человек должен сверять код с кодом, а специальные программы. Поэтому, когда я перешёл с PhpStorm на VSCode, у меня просто волосы дыбом встали от того, насколько плохо поддерживается JSDoc в VSCode. В IDEA (WebStorm, PhpStorm, ...) IDE самостоятельно связывает JSDoc-аннотации
@interfaceи@implementsв JS-коде и обеспечивает навигацию по коду, его проверку и автокомплит. И WebStorm умел это делать ещё тогда, когда TypeScirpt'а ещё не существовало.Чтобы получить аналогичное поведение в VSCode, нужно постараться. Но это и понятно, Microsoft изо всех сил развивает свои активы (TypeScript и VSCode).
tsserverне будет поддерживать JSDoc в полной мере.SolidSnack
Понял, вопросов больше не имею
flancer Автор
А вы попробуйте обсудить эту точку зрения с вашим системным аналитиком :)
SolidSnack
Системный аналитик должен знать что такое инверсия зависимости?) Или, например, почему в луковичной архитектуре стрелочки зависимостей рисуют к домену? То есть более низкоуровневый код должен зависить от более высокого?) Ну это, как будто, должны программисты знать) Почему домен не должен зависить от базы данных))
Хотя мне в комментариях один тимлид ВК высказывал что им никакая инверсия не нужна и это все кодовое графоманство) Ну я хочу сказать что и приложение ВК не лучший пример для подражания)) Да и акции уже по 130 рублей или сколько там))
flancer Автор
Системный аналитик хорошо представляет откуда берутся требования к вашему коду.
Посмотрите, что такое "I" в аббревиатуре "SOLID" (Interface Segregation Principle). Он как раз об этом - требования идут от потребителя, а не от возможностей поставщика.
SolidSnack
Ну я тоже хорошо представляю откуда берутся требования.
По вашему разделение интерфейса на более мелкие, функциональные части, это построение инверсии зависимости??
Segregation как раз переводится как разделение, и не как не связанно с направлением требований...
flancer Автор
Сформулируйте, пожалуйста, ISP в том виде, который вы используете. Я покажу, что говорит о направлении требований.
SolidSnack
Подробно можно прочитать в книге Роберта Мартина "Чистая архитектура"
flancer Автор
Вот классическая формулировка Роберта Мартина:
О направлении требований говорит "Clients should not be forced to depend...". Клиенты (потребители) не должны зависить от неиспользуемых ими интерфейсов.
Это значит, что клиенты сами определяют свою зависимость от интерфейсов, которые им нужны (that they use). Независимо ни от кого. И это уже дело среды исполнения обеспечить поставку таких компонентов, которые нужные клиенту интерфейсы имплементируют. При этом реальные возможности этих компонентов потребителя (клиента), за границами его нужд, не волнуют совершенно. Это может быть God Object, может быть "оркестр", совмещающий несколько интерфейсов, а может быть единичная имплементация именно этого интерфейса. As you wish, как говорится.
Требования к интерфейсу задаётся в местах его использования, а не в местах его имплементации.
SolidSnack
Ну я согласен что от неиспользуемых интерфейсов зависимость не нужна, это вроде очевидно.
Вы вырвали одну фразу, которая к делу, по сути, даже не относится, из книги где нужно последовательно читать главы чтобы все собрать в одну картину...
Я вас не хочу ни в чем переубедить. Я использую инверсию зависимости в своих проектах и придти к пониманию мне помогла эта книга.
flancer Автор
Ведь это и есть классическая формулировка ISP по Мартину, разве нет?
Проекты на JS или на TS? Судя по "Реально в js нет интерфейсов... ну это надо как-то проверять тогда в коде чтоли...", не на JS.
А я использую инверсию зависимостей в своих проектах именно на JS. Не на TS, не на Java, не на PHP, а именно на "ванильном" ES6+. И эта инверсия у меня такая, какая есть, именно потому, что это JS, а не TS.
SolidSnack
Вы сужаете целые главы книг до одной фразы и спрашиваете весь ли это смысл? Мне кажется очевидно что нет.
Я через день пишу на чистом js фронт, мне на нем не приходилось, пока, строить сложные вещи, но я уверен что если погрузиться в этот вопрос глубже, инверсия зависимости подругому реализуется, возможно напишу про это что-то))
dominus_augustus
Кто сейчас вообще пишет на чистом js серьезный проект.
Инверсия зависимостей в js реализуется точно также как и везде, di контейнер просто добавляет удобства.
flancer Автор
Вы считаете, что DI нет смысла использовать в несерьёзных проектах?
SolidSnack
Серьёзность проекта показывает результат, а не библиотеки
dominus_augustus
серьезность проекта показывает тот факт, планируете ли вы его поддерживать в долгосроке, тайпскрипт де-факто стандарт, если пишется лендинг условный или какой-то мини проект, то конечно ок, но средние-большие проекты без тс, ну это выстрел себе в ногу
SolidSnack
Если бы тс выполнялся в браузере, без проблем я бы лучше его взял.
А так, покрасить кнопочки, отправить ajax, мне хватает с лихвой. Бизнес логики никакой нет на фронте.
flancer Автор
Я попытался вам показать то место в книге, где говорится, что "интерфейс зарождается в коде потребителя. Это справедливо для любого ЯП." Похоже, у меня не получилось :)
С интересом прочитаю вашу точку зрения. Даже подпишусь, чтоб не пропустить :)
SolidSnack
У меня тоже не получается вам объяснить что сократить до одной фразы понятие из книги нельзя)) Не учитывая что и фразу вы приводите не ту))
Спасибо за мотивацию разобраться)
alhimik45
То есть чтобы заменить потом SqliteRepository на PostgresRepository, надо будет либо во всех зависимостях токен заресплейсить, либо поддерживать в Composition Root кастомный маппинг и поди найди потом что вообще сюда инжектится? В typescript хотя бы по имплементации интерфейса можно было бы поискать в один клик что вообще может прийти, а в чистом JS очень уж дикая неявность получается. Силиконовые конечно разберутся, но силиконовые вообще могут ручной Composition Root поддерживать который хотя бы более явный будет.
flancer Автор
Совершенно верно. Но и для TypeScript это точно также нужно будет искать и менять. И для любого друго ЯП тоже. Если вы замкнулись на конкретную имплементацию, а не на интерфейс.
Но вы можете сделать "фейковый интерфейс" в JS (вот работающий пример):
и две имплементации:
и
А в зависимостях компонентов указывать именно интерфейс
Всё, как в других ЯП.
Да, в Composition Root нужно будет решать, какая из имплементаций интерфейса в какой компонент инжектится ("везде одна и та же" или "в эти - одна, в те - другая"). Но такое решение нужно принимать везде, где указан интерфейс вместо имплементации. Я ж говорю, всё, как в других ЯП.