Ещё одна имплементация Dependency Injection в JavaScript — с ES6-модулями, с возможностью использовать один и тот же код в браузере, и в nodejs и не использовать транспиляторы.
Под катом — мой взгляд на DI, его место в современных web-приложениях, принципиальная реализация DI-контейнера, способного создавать объекты и на фронте, и на бэке, а также объяснение, при чём тут Майкл Джексон.
Очень сильно прошу тех, кому изложенное в статье покажется банальным, не насиловать себя и не читать до конца, чтобы потом, разочаровавшись, не ставить "минус". Я не против "минусов" — но только если минус сопровождается комментарием, что именно в публикации вызвало отрицательную реакцию. Это техническая статья, поэтому постарайтесь отнестись снисходительно к стилю изложения, а критиковать именно техническую составляющую изложенного. Спасибо.
Объекты в приложении
Я очень уважаю функциональное программирование, но большую часть своей профессиональной деятельности я посвятил созданию приложений, состоящих из объектов. JavaScript мне импонирует тем, что функции в нём также являются объектами. При создании приложений я мыслю объектами, это моя профессиональная деформация.
По времени жизни объекты в приложении можно разделить на следующие категории:
- постоянные — возникают на каком-то этапе работы приложения и уничтожаются только при завершении приложения;
- временные — возникают при необходимости выполнения некоторой операции и уничтожаются при завершении выполнения этой операции;
В связи с этим в программировании есть такие шаблоны проектирования, как:
- singleton;
- abstract factory, builder, pool, ...
Т.е., с моей точки зрения, приложение состоит из постоянно-существующих одиночек, которые либо сами выполняют требуемые операции, либо для их выполнения порождают временные объекты.
Контейнер Объектов
Внедрение зависимостей — это подход, который облегчает создание объектов в приложении. Т.е., в приложении существует специальный объект, который "знает", каким образом создавать все остальные объекты. Такой объект называется Контейнер Объектов (иногда — Менеджер Объектов).
Контейнер Объектов не является Божественным Объектом, т.к. его задачей является только создание значимых объектов приложения и предоставление доступа к ним другим объектам. Подавляющее большинство объектов приложения, будучи порождёнными Контейнером и размещаясь в нём, никакого представления о самом Контейнере не имеют. Их можно поместить в любую другую среду, снабдить необходимыми зависимостями и они будут также замечательно функционировать и там (тестировщики в курсе, о чём я).
Место внедрения
По большому счёту есть два способа внедрить зависимости в объект:
- через конструктор;
- через свойство (или его акцессор);
Я, в основном, использовал первый подход, поэтому дальнейшее описание я буду вести с точки зрения внедрения зависимостей через конструктор.
Допустим, что у нас есть приложение, состоящее из трёх объектов:
В PHP (этот язык с давними традициями DI у меня в данный момент находится в активном багаже, к JS я перейду чуть позже) подобная ситуация могла бы быть отражена таким образом:
class Config
{
public function __construct()
{
}
}
class Service
{
private $config;
public function __construct(Config $config)
{
$this->config = $config;
}
}
class Application
{
private $config;
private $service;
public function __construct(Config $config, Service $service)
{
$this->config = $config;
$this->service = $service;
}
}
Этой информации должно хватать, чтобы DI-контейнер (например, league/container) при соответствующей настройке смог по запросу на создание объекта Application
также создать его зависимости Service
и Config
и передать их параметрами в конструктор объекта Application
.
Идентификаторы зависимостей
Каким же образом Контейнер объектов понимает, что конструктору объекта Application
требуются два объекта Config
и Service
? Путём анализа объекта через Reflection API (Java, PHP) или через анализ непосредственно кода объекта (аннотаций к коду). То есть, в общем случае, мы можем определить имена переменных, которые ожидает увидеть на входе конструктор объекта, а если язык типизируемый, то можем получить также и типы этих переменных.
Таким образом, в качестве идентификаторов объектов Контейнер может оперировать либо именами входных параметров конструктора, либо типами входных параметров.
Создание объектов
Объект может быть в явном виде создан программистом и помещён в Контейнер под соответствующим идентификатором (например, "configuration")
/** @var \League\Container\Container $container */
$container->add("configuration", $config);
а может быть создан Контейнером по некоторым определённым правилам. Эти правила, по большому счёту, сводятся к сопоставлению идентификатора объекта его коду. Правила можно задавать явно (маппинг в виде кода, XML, JSON, ...)
[
["object_id_1", "/path/to/source1.php"],
["object_id_2", "/path/to/source2.php"],
...
]
или в виде некоторого алгоритма:
public function getSource($id)
{.
return "/path/to/source/${id}.php";
}
В PHP составление правил сопоставления имени класса файлу с его исходным кодом стандартизированы (PSR-4), в Java сопоставление идёт на уровне конфигурации JVM (class loader). Если Контейнер предусматривает автоматический поиск исходников при создании объектов, то имена классов являются достаточно хорошими идентификаторами для объектов в таком Контейнере.
Namespaces
Обычно в проекте, помимо собственного кода, используются также сторонние модули. С появлением менеджеров зависимостей (maven, composer, npm) использование модулей очень сильно упростилось, а количество модулей в проектах очень сильно увеличилось. Пространства имён позволяют существовать в едином проекте одноимённым элементам кода из различных модулей (классы, функции, константы).
Есть языки, в которых пространство имён встроено изначально (Java):
package vendor.project.module.folder;
есть языки, в которых пространство имён добавлено в ходе развития языка (PHP):
namespace Vendor\Project\Module\Folder;
Хорошая реализация пространства имён позволяет однозначно адресовать любой элемент кода:
\Doctrine\Common\Annotations\Annotation\Attribute::$name
Пространство имён решает задачу по упорядочиванию множества программных элементов в проекте, а файловая структура решает задачу по упорядочиванию файлов на диске. Поэтому между ними не просто много общего, а иногда и очень много — в Java, например, публичный класс в пространстве имён однозначно должен быть привязан к файлу с кодом этого класса.
Таким образом, использование в Контейнере в качестве идентификаторов объектов идентификатора класса объекта в пространстве имён проекта является хорошей идеей и может служить основой для создания правил по автоматическому обнаружению исходных кодов при создании требуемого объекта.
$container->add(\Vendor\Project\Module\ObjectType::class, $obj);
Автозагрузка кода
В PHP composer
пространство имён модуля маппится на файловую систему внутри модуля в дескрипторе модуля composer.json
:
"autoload": {
"psr-4": { "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" }
}
JS-сообщество могло бы делать аналогичный маппинг в package.json
, если бы в JS были пространства имён.
Идентификаторы зависимостей в JS
Выше я обозначил, что в качестве идентификаторов Контейнер может использовать либо имена входных параметров конструктора, либо типы входных параметров. Проблема в том, что:
- JS — язык с динамической типизацией и не предусматривает указание типов при объявлении функции.
- В JS используются минификаторы, которые могут переименовывать входные параметры.
Разработчики DI-контейнера awilix предлагают использовать объект в качестве единственного входного параметра конструктора, а в качестве зависимостей — свойства этого объекта:
class UserController {
constructor(opts) {
this.userService = opts.userService
}
}
Идентификатор свойства объекта в JS может состоять из буквенно-цифровых символов, "_" и "$", причем не может начинаться с цифры.
Так как нам для автозагрузки нужно будет мапить идентификаторы зависимостей на путь к их исходникам в файловой системе, то лучше отказаться от использования "$" и воспользоваться опытом PHP. До появления оператора namespace
в некоторых framework'ах (например, в Zend 1) использовали такие наименования для классов:
class Zend_Config_Writer_Json {...}
Таким образом, мы могли бы отразить наше приложение из трёх объектов (Application
, Config
, Service
) на JS как-то так:
class Vendor_Project_Config {
constructor() {
}
}
class Vendor_Project_Service {
constructor({Vendor_Project_Config}) {
this.config = Vendor_Project_Config;
}
}
class Vendor_Project_Application {
constructor({Vendor_Project_Config, Vendor_Project_Service}) {
this.config = Vendor_Project_Config;
this.service = Vendor_Project_Service;
}
}
Если мы размещаем код каждого класса:
export default class Vendor_Project_Application {
constructor({Vendor_Project_Config, Vendor_Project_Service}) {
this.config = Vendor_Project_Config;
this.service = Vendor_Project_Service;
}
}
в своём файле внутри модуля нашего проекта:
./src/
./Application.js
./Config.js
./Service.js
То мы можем связать корневой каталог модуля с корневым "namespace'ом" модуля в конфигурации Контейнера:
const ns = "Vendor_Project";
const path = path.join(module_root, "src");
container.addSourceMapping(ns, path);
а затем, отталкиваясь от этой информации, конструировать на основании идентификатора зависимости (Vendor_Project_Config
) путь к соответствующим исходникам (${module_root}/src/Config.js
).
ES6 Модули
ES6 предлагает общую конструкцию для загрузки ES6-модулей:
import { something } from 'path/to/source/with/something';
Так как нам нужно один объект (класс) привязывать к одному файлу, то есть смысл в исходнике экспортировать этот класс по-умолчанию:
export default class Vendor_Project_Path_To_Source_With_Something {...}
В принципе, можно не писать такое длинное имя для класса, достаточно просто Something
и тоже будет работать, но в Zend 1 писали и не переломились, а уникальность имени класса в пределах проекта положительно сказывается как на возможностях IDE (autocomplete и контекстные подсказки), так и при отладке:
Импорт класса и создание объекта в таком случае выглядит так:
import Something from 'path/to/source/with/something';
const something = new Something();
Front & Back импорт
Импорт работает как в браузере, так и в nodejs, но есть нюансы. Например, браузер не понимает импорта nodejs-модулей:
import path from "path";
В браузере получаем ошибку:
Failed to resolve module specifier "path". Relative references must start with either "/", "./", or "../".
То есть, если мы хотим, чтобы наш код работал и в браузере, и в nodejs, мы не можем использовать конструкции которые не понимает браузер или nodejs. Я специально акцентирую на этом внимание, потому что такой вывод слишком естественен, чтобы о нём думать. Как дышать.
Место DI в современных web-приложениях
Это сугубо моё личное мнение, обусловленное моим персональным опытом, как и всё остальное в этой публикации.
В web-приложениях JS практически безальтернативно занимает своё место на фронте, в браузере. На серверной стороне плотно окопались Java, PHP, .Net, Ruby, python,… Но с появлением nodejs JavaScript также проник и на сервер. А технологии, используемые в других языках, в том числе и DI, начали проникать в серверный JS.
Развитие JavaScript обусловлено асинхроностью работы кода в браузере. Асинхронность не является исключительной особенностью JS, скорее врождённой. Сейчас наличие JS и на сервере, и на фронте уже никого не удивляет, а скорее, стимулирует к использованию одних и тех же подходов на обоих "концах" web-приложения. И одного и того же кода. Разумеется, что фронт и бэк слишком различаются по своей сути и по решаемым задачам, чтобы использовать один и тот же код и там, и там. Но можно предположить, что в более-менее сложном приложении будет код браузерный, серверный и общий.
DI уже сейчас используется на фронте, в RequireJS:
define(
["./config", "./service"],
function App(Config, Service) {}
);
Правда тут идентификаторы зависимостей прописываются в явном виде и сразу в виде ссылок на исходники (можно настроить маппинг идентификаторов в конфиге загрузчика).
В современных web-приложениях DI существует не только на серверной стороне, но и в браузере.
При чём тут Майкл Джексон?
При включении поддержки ES-модулей в nodejs (флаг --experimental-modules
) движок идентифицирует содержимое файлов с расширением *.mjs
как EcmaScript-модули (в отличие от Common-модулей с расширением *.cjs
).
Иногда такой подход называют "Michael Jackson Solution", а скрипты — Michael Jackson Scripts (*.mjs
).
Согласен, что так себе интрига с КДПВ разрешилась, но… камон ребят, Майкл Джексон...
Yet Another DI Implementation
Ну и как полагается, собственный велосипед DI-модуль — @teqfw/di
Это не готовое "к бою" решение, а скорее принципиальная реализация. Все зависимости должны представлять из себя ES-модули и использовать общие для браузера и nodejs возможности.
Для разрешения зависимостей в модуле применяется подход awilix:
constructor(spec) {
/** @type {Vendor_Module_Config} */
const _config = spec.Vendor_Module_Config;
/** @type {Vendor_Module_Service} */
const _service = spec.Vendor_Module_Service;
}
Для запуска back-примера:
import Container from "./src/Container.mjs";
const container = new Container();
container.addSourceMapping("Vendor_Module", "../example");
container.get("Vendor_Module_App")
.then((app) => {
app.run();
});
на сервере:
$ node --experimental-modules main.mjs
Для запуска front-примера (example.html
):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DI in Browser</title>
<script type="module" src="./main.mjs"></script>
</head>
<body>
<p>Load main script './main.mjs', create new DI container, then get object by ID from container.</p>
<p>Open browser console to see output.</p>
</body>
</html>
нужно выложить модуль на сервер и открыть страницу example.html
в браузере (или воспользоваться возможностями IDE). Если открывать example.html
напрямую, то в Chrom'е ошибка:
Access to script at 'file:///home/alex/work/teqfw.di/main.mjs' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.
Если всё прошло удачно, то в консоли (браузера или nodejs) будет примерно такой вывод:
Create object with ID 'Vendor_Module_App'.
Create object with ID 'Vendor_Module_Config'.
There is no dependency with id 'Vendor_Module_Config' yet.
'Vendor_Module_Config' instance is created.
Create object with ID 'Vendor_Module_Service'.
There is no dependency with id 'Vendor_Module_Service' yet.
'Vendor_Module_Service' instance is created (deps: [Vendor_Module_Config]).
'Vendor_Module_App' instance is created (deps: [Vendor_Module_Config, Vendor_Module_Service]).
Application 'Vendor_Module_Config' is running.
Резюме
ESM!
Комментарии (39)
ReklatsMasters
22.08.2019 01:52+2Мне кажется, что вы создаёте проблемы на ровном месте и сложным образом решаете. Я не понимаю зачем юзать DI, когда es модули удобны и переносимы сами по себе.
Кстати, от mjs отказались, теперь в package.json просто будет указывается type: module.
VolCh
22.08.2019 06:37+1А как вы в модули инжектите их зависимости с возможностью их переопределения не трогая кода модуля?
ReklatsMasters
22.08.2019 09:23jest на этапе тестирования это умеет из коробки.
flancer Автор
22.08.2019 10:20Думаю, что такое возможно, если
jest
берёт на себя функцию загрузки модулей и подменяет модули моками на лету. Вот только когда я пытался создать тестовое окружение для разработки ES-модуля при помощиjest
пришлось перейти наmocha
— там это делается гораздо проще. Я не говорю, чтоjest
не работает с ESM, я не изучал этот вопрос. Я лишь говорю, что подружитьmocha
с ESM гораздо проще, чемjest
. Возможно, что как раз из-за этого умения.
flancer Автор
22.08.2019 06:45ES-модули — это прежде всего код, исходники. А DI — это окружение (рабочее, девелоперское, тестовое). Контейнер содержит уже настроенные для функционирования объекты, связанные между собой тем или иным образом. В языках с интерфейсами можно подменять их имплементации в Контейнере на уровне конфигурации Контейнера.
Я использовал DI в Java и PHP, а там нет es-модулей. Так что основной ответ на вопрос "зачем" — мне так удобнее :)
Кстати, от mjs отказались, теперь в package.json просто будет указывается type: module.
Это работает, начиная с v12, 11-я версия ноды всё ещё требует наличия
*.mjs
даже сtype:module
.
risedphantom
22.08.2019 09:13+1Zanuda mod.on
"Инверсия управления (англ. Inversion of Control, IoC) — важный принцип объектно-ориентированного программирования, используемый для уменьшения зацепления в компьютерных программах. Одной из реализаций инверсии управления в применении к управлению зависимостями является внедрение зависимостей (англ. dependency injection). Внедрение зависимости используется во многих фреймворках, которые называются IoC-контейнерами." ©
DI же в двух словах — вместо require/import модуля вы инжектируете зависимость через параметр конструктора (или сеттер свойства). То есть за этим громким словом стоит простое "передавайте зависимости класса через параметры конструктора".
Zanuda mod.off
sanchezzzhak
22.08.2019 13:36Для серверной разработки di не нужен, у нас есть микросерисы, которые подключают все что нам нужно в рамках сервиса, меня этот расклад устраивает на 100%.
Для фронта, я бы хотел видить di загрузку кода по требованию без костылей, но и за большого зоопарка андройдов с хромом 45 и ниже версии, мы не можем использовать es6 и выше, только через babel (приходится делать 2 версии)
на Type script ecть http://inversify.io/ зависимости подключают через декоратор, что очень симпатично.
funca
24.08.2019 15:42Подключение через декоратор автоматически тащит зависимостью конкретную реализацию DI. Поэтому подробного рода штуки получили распространение лишь во фреймворках, где все разработчики не против такого соглашения. Даже RequireJS в итоге не взлетел.
AlexSpaizNet
22.08.2019 15:02+1Возможно это только мой опыт, но когда перешел в мир жаваскрипта из PHP/Python/Java/Go, очень тяжело объяснять разработчикам зачем нужен DI (я не имею ввиду фреймворк, я имею ввиду чистый ручно инжекшен зависимостей). Ну привыкли они делать require() и все работает. Тесты? Привет rewire, proxyquire и другим костылям… которые тоже не со всем справляются когда весь проект выглядит как клубок из require(), имеются cycle dependencies и код выполняется прямо после вызова require(), а не когда Я как разработчик захочу…
А когда количество тестов растет и они начинают падать непонятно почему (понятно, где то что то замокалось один раз и все, потому что референс на функцию зарезолвился при require(), потому что никто не понимает что тесты бегут в одном процессе...), когда что бы протестировать хоть что то нужно делать танцы с бубном… я начинаю плакать Ж(
Как же все таки приятно работать когда я могу заинжектить то что мне надо, даже не используя сторонних библиотек…
У меня есть теория, что если человеку не приходилось заниматься сексом с огромным количеством тестов, ему трудно понять преимущества этого подхода…
GerasimGerasimov
22.08.2019 17:19Не по теме, но чтобы на фронте и сервере использовать CommonJS модули и продвинутые ООП возможности, я по совету старших перешёл на TypeScript
flancer Автор
22.08.2019 17:40Мне на TS перейти религия не позволяет — я транспиляторы не люблю :)
symbix
22.08.2019 20:34После того, как TS мне через секунду после написания кода нашел ошибку (отсутствие обязательного ключа в большом таком объекте) в цепочке из этак 10 rxjs-операторов, я не понимаю, как можно это не любить. Вручную я бы это дебажил полчаса точно — да и не факт, что вообще заметил бы! (Тестами там покрыть затруднительно, обвязка вокруг двух сильно навороченных сторонних API).
symbix
Ха, какая изящная маскировка Service Locator-а под DI. Даже может показаться, что это DI! :-)
flancer Автор
А чем, по вашему, true DI отличается от Service Locator, замаскированного под DI?
VolCh
Доступом к контейнеру
flancer Автор
Поясните, пожалуйста, свою версию различий на примере. Очень не хотелось бы получить вместо DI-контейнера контейнер Service Locator'а (весь интернет забит информацией, что это жуткий анти-паттерн).
Может ещё что-то можно исправить, кода-то в реализации совсем немного — 250 строк где-то. Ну или переимновать di-модуль в sl-модуль, если исправление принципиально невозможно.
VolCh
Я исхожу из того, что в коде
spec иметт множество полей, а не исключительно Vendor_Module_Service и Vendor_Module_Config. Ну вот как-то сложилось такое впечатление. Если spec генерируется исключительно для этого класса, какждый раз когда нужен его инстанс, то таки ближе к true DI
flancer Автор
Это просто я привёл более классическую запись объекта со свойствами, можно конструктор записать и так:
можно даже не вводить промежуточные константы, а напрямую обращаться к:
symbix
Изящность (я тут без иронии) вашего варианта в том, что вы "взломали" мой стандартный способ объяснения, чем DI отличается от SL:
DI:
SL:
Но, в принципе, оно справедливо и с вашим вариантом — ведь за элегантным трюком стоит именно второе.
Если в целом — то в DI контейнер решает, что именно заинжектить, а в SL класс решает, что к себе втащить.
Если детально — DI позволяет:
1) создать всю цепочку зависимостей рекурсивно "по требованию" (в принципе, это и в вашем варианте решаемо через геттеры или proxy),
2) завязываться на интерфейсы, а не на конкретные реализации (это, в принципе, у вас тоже можно),
3) поддерживать разные варианты инстанциирования (вот тут синглтон, а вот тут новый инстанс на каждое обращение) — это, в принципе, тоже у вас можно (см.п.1), но — только глобально (см. п. 5),
4) сохранить возможность прямого создания объекта безо всяких там контейнеров, тупо написав new Foo(dep1, dep2) — ну, с поправкой на лишние скобочки, это, положим, есть,
5) инжектить разные реализации в зависимости от контекста (Foo и Bar хотят CacherInterface, в Foo я хочу MemcachedCacher, а в Bar я хочу RedisCacher), либо сделать "везде синглтон, но вот для Baz — новый инстанс" — вот тут уже облом,
6) подсовывать в конструктор иные аргументы, которые не зависимости (скажем, есть какой-нибудь аргумент defaultTimeout у http-клиента) — тоже облом (хотя это сомнительная фича).
И, да, как уже справедливо заметили, любой DI можно использовать как SL (в конце концов, даже при правильном использовании как минимум один раз — во входной точке — он именно так и используется). Но не наоборот.
flancer Автор
Примерно понял. Спасибо. Но в том-то и дело, что сам контейнер в классы не передаётся. Т.е., нет такого:
spec
— это примитивный объект, который в качестве properties содержит зависимости конструируемого объекта:Контейнер берёт конструктор объекта, а затем запускает процесс создания объекта, передавая в конструктор прокси-объект, который на запрос значения соотв. свойства (dep1 | dep2) либо возвращает уже готовый объект (Object1 | Object2), если он есть в контейнере, либо запускает процесс создания соотв. зависимости (Object1 | Object2).
Т.е., конструируемый объект ничего не знает о Контейнере. Он предполагает, что зависимости передаются ему упакованными в объект
spec
. Весьmagic
с прокси-объектом и созданием объекта я взял у awilix. Я бы и весьawilix
взял, только я не увидел, как его можно заюзать в браузере. А мне интересно было затянуть DI в SPA, пришлось брать только самую "мякотку".5 и 6 пункты нужно будет помозговать. Пока что просто интересно сравнить "классический" DI (PHP, Java /с .Net не работал/) и JS'овский (с учётом отсутствия типизации параметров функций и возможности минификации кода).
VolCh
А у вас контейнер только синглтоны позволяет и только через new ?
flancer Автор
Да.
symbix
Вот как бы и передается, и не передается — смотря как посмотреть! Если посмотреть в ES5 после транспайлера — наверняка получится, что передается. :-) В любом случае, тут зависимость от контейнера, хоть и неявная: класс сам определяет, что именно взять из контейнера (а что это красиво завернуто в синтаксический сахарок — это принципиально ведь не меняет ничего).
Можно представить себе такое использование DI, в котором одни синглтоны и все зависимости определены по конкретному классу (без абстрактных классов или интерфейсов) — вот будет примерно оно.
Что касается типизации — я мыслю Typescript-ом :-) И тут, кстати, обнаружил, что его особенность (type erasure) прекрасно ложится на старое и уже позабытое понимание отличий абстрактного класса (включая pure) от интерфейса. В старых книжках начала 90-х все было строго: (абстрактный) класс — это is-a (скажем, Logger), а интерфейс — это can (скажем, Countable или Comparable). Исходя из этого, получается, что зависимости логично делать как раз от [pure] abstract class, и тут как раз никакой type erasure не мешает (и все эти трюки с символами совершенно ни к чему).
В голом JS — ну, пусть даже токеном будет не абстрактный класс, а строка (которая ключик объекта), но как понять, чей именно конструктор запросил контейнер (вот она, сервислокаторная сущность вылезла: в DI-то по определению известно!)?
symbix
А, вот еще мысль.
Как у вас выглядит самый первый вызов, во входной точке?
Если new SomeClass(container) — так вот он, сервис-локатор самый что ни на есть.
А если container.make(SomeClass) — то, кажется, все вполне решаемо, если завести стек — и вроде прям всамделишный DI получится.
flancer Автор
symbix
Ну, то есть если для упрощения откинуть асинхронщину, то будет container.get(App).run().
Вроде все решаемо!
flancer Автор
Здесь нет транспиляции. Это чистый JS.
symbix
Ну, может, я IE захотел. :-) Ок, плохой пример, давайте посмотрим в опкоды V8. :-)
justboris
Я открыл исходник и вижу Proxy вместо примитивного объекта. То есть, у вас все-таки получается container, замаскированный через Proxy. Работать будет, но тем не менее.
P.S. с уровнем вложенности в этом коде просто беда… Что помешало использовать async-функции?
flancer Автор
В принципе можно сделать парсинг аргументов конструктора такого вида:
и обойтись без прокси. Это будет
true DI
в таком случае?Суть DI не в том, как я вставляю зависимости в объект, а в том, что сам объект ничего не знает, как я вставляю зависимости в него.
Я пока ещё не мыслю asynchronously — у меня очень массивный Java/PHP background синхронного программирования. Плюс замыкания — я там тоже не силён. Сделал так, как оно работало.
justboris
Не обязательно что-то переделывать, нужно просто называть вещи своими именами.
Да, есть контейнер, но он создается под каждый класс отдельно, поэтому недостатками классического глобального service locator он не обладает.
justboris
Здесь не нужно как-то по другому мыслить. Был такой код
стал такой
Получается компактнее и читаемее.
flancer Автор
Не получается :(
symbix
Ну вот конкретно для Ноды я вообще не понимаю, зачем заморачиваться с асинхронщиной для модулей. (Для браузера — понятно)
justboris
Правильно, нужно саму функцию отметить как async
Проблема в том, что у вас там еще пара замыканий на пути, и их тоже нужно переделать… Но миграция на async-функции стоит того. Вот материал на эту тему с самым большим количеством плюсов: https://habr.com/en/company/ruvds/blog/326074/
flancer Автор
Это из wiki:
Как ниже заметил коллега risedphantom :
По-простому если, то
Service Locator
самый что ни на естьtrue DI
, если он при создании объектов "передаёт зависимости через параметры конструктора". "Мокрое" вполне может быть одновременно и "зелёным".Коллега VolCh вообще считает, что DI'ность контейнера не "врождённое" свойство, а зависит от того, как мы его используем. Один и тот же контейнер может одними разработчиками использоваться как DI-контейнер, а другими — как "не-DI, но тоже очень хороший", контейнер.