Комментарии 25
Зачем Javasript-у DI контейнер?
Да по той же причине по которой его использует Nest.js или Angular. У современных приложений очень высокая сложность. По мере роста проекта сервисы начинают зависеть от репозиториев, логгеров, клиентов API, контролировать все связи становится сложно, а при необходимости сменить один логгер на другой можно испытать сильную боль. В этом JS не отличается от других языков. JS и Node.js давно перестали быть просто скриптовыми языками.
а при необходимости сменить один логгер на другой можно испытать сильную боль

Нужен. Очень не хватает. Конечно, все себе по разному представляют js, но если отбросить предрассудки типа «корочки красят в своих интерфейсах», то вполне понятно зачем. JS такой же ЯП как и многие другие, с такими же примерно проблемами, на нем решают примерно такие же задачи и т.д.
Зачем? Примеры кода можете? Чтобы отчетливо было видно что DI-контейнер это прям революция?
DI-контейнер - это никакая не революция. Им сто лет в обед. Я со Spring'ом работал в 2010-х. Это уже была 3-я версия платформы. Затем я использовал DI в PHP - в Magento. Очень своеобразный DI, особенно в первой версии (2008 - 2015 год). Использовал DI из Symfony 2. В PHP вообще с этим делом всё хорошо, даже стандарт есть - PSR-11.
Когда окончательно перешёл в JS даже "запилил" свой DI - чтобы писать в привычной для себя манере. Правда я позволил ИИ его "под себя" переписать (под "силиконовых", а не под "кожаных").
Что касается примеров кода, то могу предложить посмотреть вот этот код - @flancer32/teq-cms. Я писал его "ручками", ещё на "рукописном" DI. А код в @flancer32/teq-web уже написан ИИ (Codex) с "его" DI. Я в код заглядывал, но руками старался не править ничего. Самый, что ни на есть "машинный код".
Рабочий код на фронте - вот. Зайдите в DevTools и посмотрите, что видит браузер:

Это старый код, но сделан тоже в той же IoC-парадигме с моим рукописным DI в основе. Для построения UI используется vue и Quasar.
Если вы посмотрите все es6-модули, то там не будет ни одного (!) статического импорта. Статические импорты нужны в головном скрипте для загрузки DI-библиотеки, а дальше контейнер сам динамически (через import()) загружает нужные зависимости по мере необходимости. Как следствие, код становится изоморфным (без изменений может использоваться как в браузере, так и в nodejs). Вот вообще без изменений! Поначалу я на бэке импортировал node'овские библиотеки, но сейчас и они инжектятся. То есть, можно и бэкендовый код в браузере запускать, только node-библиотеки нужно замокать (делается через препроцессор контейнера). А сам код приложения остаётся без изменений.
И в отличие от многих других DI-контейнеров (включая контейнер автора статьи), у моего контейнера ограничена возможность что-то зарегистрировать специально:
const container = new Container()
.registerValue(ID_GENERATOR, randomUUID)
.registerValue(CLOCK, () => new Date())
.registerClass(CartApplicationService);У меня регистрировать можно только если перевести контейнер в тестовый режим:
container.enableTestMode();
container.register("App_Service$", mockService);То есть, в prod-режиме все es6-модули резолвятся и подгружаются автоматически, сколько бы их там ни было. Нужно только "потянуть" головной модуль, а дальше строится дерево зависимостей и подтягиваются исходники (можно прямо с unpkg.com тянуть). Более того, с учётом наличия в контейнере препроцессора, можно менять маппинг идентификаторов зависимостей на исходный код и реализовывать такие посторонние для "ванильного" JS вещи, как интерфейсы. А с учётом наличия там же постпроцессора, можно использовать в JS и AOP.
Все эти приёмы и технологии давным давно используются в программировании и никакая не революция. Просто они используются там, где кодовая база проекта начинает заваливать за миллионы строк кода (в Magento 2, например, больше 2 млн. и для работы с JS используется requirejs, тоже своего рода DI).
Много у вас примеров таких объёмных кодовых баз именно на JavaScript? А для кодирования на одной веб-страничке чего угодно такие навороты действительно не нужны.
Spring - это понятно. Это Java и приколоченное гвоздями ООП. А в Javascript то зачем? В Javascript классы это просто "синтаксический сахар". Зачем вообще тащить в программирование на Javascript ООП шаблоны? В нем без них значительно проще, быстрее и дешевле.
Javascript классы это просто "синтаксический сахар"
Предрассудки...
class Bar {
method1() {
// ...
}
method2() {
// ...
}
}
class Foo extends Bar {
// Static initialization block
static {
try {
} catch {
}
}
// Private static field
static #field = 1;
// Private static method
static #method() {}
// Private static getter
static get #foo() {
return this.#privateField;
}
// override parent method
method1() {
// ...
}
// extend parent method
method2() {
super.method2()
// ...
}
// child private method
#method() {}
}Подскажите, это «синтаксический сахар» над чем?
Вы приявязыветесь к языку, паттерны же решают какой-то класс задач и language-agnostic. Так например signals, reactor, observable, тот же di, они сейчас в разных библиотеках и вы ими неосознанно пользуетесь каждый день. На счёт классов, они стали не совсем синтаксическим сахаром, сейчас есть небольшие различия во внутренней реализации, но это тоже не важно. Прототипы и наследование через них просто один из вариантов реализации ООП. Мне лично нравится объяснение дядюшки Боба про различия разных парадигм.
Все! Я понял. Вы не можете без ООП. А Javascript прекрасен как раз тем, что на нем можно быстро, дешево и качественно делать код без ООП. Поэтому я на него и пересел после C++, потом Java, потом C#, потом Python...
DI - это про внедрение зависимостей. Зависимости есть в любом коде, состоящем из более, чем одного файла. Статические импорты в JS - это тоже зависимости.
DI в JS можно вообще без классов использовать:
// src/App/Config.mjs
export default () => ({
greeting: 'Hello',
punctuation: '!',
});// src/App/FormatName.mjs
export default () => (name) => {
const value = name.trim().toLowerCase();
return value.charAt(0).toUpperCase() + value.slice(1);
};// src/App/Greet.mjs
export const __deps__ = {
default: {
config: 'App_Config$',
formatName: 'App_FormatName$',
},
};
export default ({config, formatName}) =>
(name) => `${config.greeting}, ${formatName(name)}${config.punctuation}`;// bootstrap.mjs
import Container from '@teqfw/di';
const container = new Container();
container.addNamespaceRoot(
'App_',
new URL('./src/App/', import.meta.url).href,
'.mjs'
);
const greet = await container.get('App_Greet$');
console.log(greet(' aLEX '));Вот архив с кодом - достаточно распаковать, затем npm install & npm start.
Если кодовая база большая, то нужен какой-то механизм для связывания отдельных файлов. В JS для этого используют импорты - статические и динамические. DI - это как раз и есть связывание отдельных исходников в единую кодовую базу через динамический импорт. ООП тут вообще перпендикулярно стоит ко всему этому.
Можно вас попросить привести нормальный пример, когда просто импорта зависимости не хватает и вам нужен именно DI? Какую проблему вы решаете с помощью DI?
Смотрите, я привёл код, который "на пальцах" демонстрирует идею позднего связывания и создания JS-кода без статических импортов. А вы говорите, дайте "нормальный пример". Я уже дал. Просто вы не видите, где его можно применить. Я дам вам другой пример (вот интерфейс, вот место применения, а вот - имплементация в другом пакете), и вы скажете, что это же можно сделать и со статическими импортами. И будете совершенно правы. А кто-то скажет, что это же можно сделать и на любом другом ЯП или даже сразу в машкодах. И тоже будет совершенно прав.
Дело не в правоте, а в возможности решать задачи наиболее удобным для вас путём. Если вы не видите, где применять JS-код без статических импортов или использовать позднее связывание - вам просто это не нужно. Было бы нужно, вы бы зацепились глазом за пример выше и вас бы сразу триггернуло. Я вас уверяю, это и есть нормальный пример. Там всё видно. По крайней мере тем, кому это надо или может быть надо.
Какую проблему вы решаете с помощью DI?
Лично я решаю с помощью DI проблему "распределённой разработки кода слабосвязанными командами разработчиков". Я очень быстро забываю контекст проекта. Через полгода уже не помню ничего, если в течение этого времени не заглядывал код. Так что "я сейчас" и "я через год" - это и есть слабосвязанные команды разработчиков :)
А в принципе, весь IoC для этого был придуман. DI - лишь один из вариантов.
Как только у вас появляется container.register, это уже service locator. Вообще, все попытки реализовать тру di в js в конечном итоге сводятся к созданию локатора.
В вашем решении меня смущают анонимные классы, я привык к instanceof.
Передача параметров объектом тоже смущает. Аргументы в пользу — убедительные, но это софистика. Дело в том, что если мне нужен один-два аргумента, я не запутаюсь, а если 10, то стоит пересмотреть этот класс, с ним явно что-то не так.
Но попытка интересная.
С утверждением про register() я не до конца согласен. Само наличие реестра и метода resolve() еще не делает решение Service Locator. Для меня граница проходит в месте использования контейнера. Если контейнер передается внутрь прикладного кода и класс сам вызывает resolve(), зависимости действительно становятся скрытыми и получается локатор. В примере контейнер остается в composition root, а сервис получает конкретный набор зависимостей через конструктор и вообще не знает о существовании контейнера. Собственно для меня это самое важное отличие - про локатор знают все.
При этом, технически, контейнер внутри себя работает как реестр и механизм поиска объектов. Поэтому разница здесь скорее архитектурная: кто управляет разрешением зависимостей, внешний код или сам сервис.
Классы в примере анонимные только в выражении, из которого они создаются. component() создает класс один раз, задает ему имя и возвращает стабильную ссылку. Поэтому обычный instanceof работает:
const service = container.resolve(CartApplicationService);
service instanceof CartApplicationService; // trueНовый класс при каждом resolve() не создается.
Передача зависимостей объектом не является обязательной частью идеи. Контейнер вполне можно переделать на позиционные аргументы. Мне объект показался удобнее из-за именованных зависимостей и отсутствия привязки к порядку, но аргумент про один-два параметра справедлив. Обычно в роде всегда использую обеъкт как единственный параметр, так намного проще вносить изменения - но чисто вкусовщина конечно.
Спасибо за статью! Я тоже пытался решить эту проблему: https://docs.cleverbrush.com/di
напишу статью про мой опыт.
DI выглядит как антипаттерн. Волосы стынут в жилах, когда в рантайме решается, взлетит приложуха или нет
У нас в проде NestJS, там reflect-metadata и decorators, и один раз получили сюрприз: перевели сборку на esbuild ради скорости, а decorators metadata сломались, потому что esbuild эмитит их не так как tsc. Две недели дебага, прежде чем поняли что дело не в коде. Ваш подход с explicit inject через обычные объекты от этого класса проблем избавляет полностью, ценой ручного описания зависимостей.


DI-контейнер на чистом JavaScript без TypeScript и reflect-metadata