Прочитав несколько статей на тему DI мне захотелось использовать его в Node.js; после недолгих поисков оказалось, что модулей для этого не так много, из них самый интересный — di.js от Angular, но он мне не подошел и я решил написать свой.
Почему не di.js?
- он написан с использованием ES6, т.е. нуждается в предварительной компиляции в ES5, а так как он использует декораторы, то и ваш код должен быть скомпилирован в ES5
- давно не поддерживается
- использует старый компилятор (es6-shim)
- нет возможности создавать несколько инстансов одного класса
Пишем свою реализацию
Самой интересной в написании модуля оказалась задача динамической передачи в конструктор аргументов.
Наиболее очевидное решение — использовать apply, но это не сработает, так как он взаимодействует с методами, а не конструкторами.
Для нашей цели можно воспользоваться spread operator:
class Test {
constructor(a, b) {
}
}
let args = [1, 2]
let test = new Test(...args)
В остальном реализация довольно скучна и не содержит ничего интересного.
Используем модуль
Я решил отказаться от декораторов di.js в пользу конфиг-файла. Допустим, мы описываем архитектуру компьютера, тогда в конфиге нам нужно описать наши классы, пути к ним и их аргументы:
{
"Computer": {
"class": "./Computer.js", // Путь к классу
"args": ["RAM", "HDD", "CPU", "GPU"] // Зависимости
},
"RAM": {
"class": "./RAM.js",
"args": []
},
"HDD": {
"class": "./HDD.js",
"args": []
},
"CPU": {
"class": "./CPU.js",
"args": ["RAM"]
},
"GPU": {
"class": "./GPU.js",
"args": []
}
}
Класс Computer, как и все остальные, довольно простой:
'use strict'
class Computer {
constructor(ram, hdd, cpu, gpu) {
this._cpu = cpu
this._gpu = gpu
this._hdd = hdd
this._ram = ram
}
}
module.exports = Computer
Теперь в точке входа нашего приложения используем модуль:
const Injector = require('razr')(__dirname, './path/to/your/config.json') // передаем текущий путь и путь к конфигу
const computer = Injector.get('Computer') // тут мы молучим инстанс Computer
Стоит заметить, что в конфиг-файле нужно указывать пути к классам относительно точки входа приложения.
На этом все. Спасибо всем, кто дочитал. А вот ссылочка на GitHub — https://github.com/Naltox/razr и NPM — http://npmjs.com/package/razr
Комментарии (34)
samizdam
23.07.2016 19:10+1Насколько я понял из исходного кода, вы реализовали Service Locator, который инстанцирует сервисы-одиночки на основе захардкоженной в виде строк конфигурации.
Аналогичного результат можно было бы достигнуть в объекте конфига сразу произведя инстанцирование.
Имхо декораторы выглядят перспективнее для решения задачи DI. А в чём проблема компилировать свой node.js код в ES5?Altox
23.07.2016 19:17+1Я согласен, что декораторы выглядят перспективнее, но мне не очень нравится идея компилировать серверный код ради пары фич.
А еще с декораторами нельзя будет сделать несколько инстансов одного класса (разные коннекты к дб, например)AndreyRubankov
24.07.2016 00:01+1Через декораторы несколько разных инстансов сделать не получится, да.
Но в контексте DI, если вам нужны разные инстансы коннекторов — они описываются в конфиге, а в сервисы вы уже подтягиваете нужный инстанс:
function Inject(field, instanceName) { return function(target) { target[field] = CONFIG[instanceName]; } } @Inject("pgConnection", "pgConnector") @Inject("mongoConnection", "mongoConnector") class MyService { var pgConnection; var mongoConnection; }
AndreyRubankov
23.07.2016 23:23Идея интересная, но содержит неприятную проблему: в одном файле может быть несколько классов и ни один из них не будет называться по имени файла (файл — это модуль, внутри множество классов).
На небольших проектах, где количество классов будет стремиться к десяткам Ваш подход через конфигурацию будет вполне удобным, для большого проекта написать конфигурацию будет ужасной болью.
А рефакторить и всячески поддерживать данный подход будет еще больнее.
Пока в JS | ES не будет полноценной поддержки метаданных (декораторов, аннотаций), реализовать полноценный DI будет практически невозможно. А при отсутствии типов поддерживать и отлаживать такой DI контейнер будет сложно.
AndreyRubankov
23.07.2016 23:30Вы как-то сами себе противоречите:
Почему не di.js?
- он написан с использованием ES6, т.е. нуждается в предварительной компиляции в ES5
И следом:
Для нашей цели можно воспользоваться spread operator
Spread operator — это ES6 стандарт, как и Классы и let, которые идут в примерах.Altox
23.07.2016 23:37+1И Spread operator и классы и let поддерживаются последней нодой, их не нужно компилировать.
AxVPast
24.07.2016 12:231. есть вариант с применением requirejs — очень не плохой (правда там не решают проблему №2), кстати очень напоминает spring.
2. оно не работает с аснихронным кодом — верно? То есть для примитивных поделок сойдет, но как только Вам потребуется получить объект который что-то читает из БД (для инициализации) или тянет с файловой системы файл (не через readFileSync) то тут будет проблема.
В общем в правильном варианте стандартным new не обойтись — нужен еще метод init который возвращает обещание. Также, как обычно нужен метод shutdown, так как де-инициализация тоже важная часть жизненного цикла программы, о которой, зачастую, забывают.symbix
24.07.2016 13:52А зачем там работа с асинхронным кодом? Контейнер ничего, кроме вызова конструкторов, делать не должен. Если конструктор возвращает promise — это какой-то очень странный дизайн :)
AxVPast
24.07.2016 15:31Простой понятный пример — рисуем баннеры на которых написаны буквы. При этом код должен работать всегда, а не в некоторых теплично-лабораторных условиях. Другими словами у «тормоза»-разработчика все работает, так как он не умеет сразу (время порядка 10мс и меньше) после listen сделать запрос к баннеру. Понятное дело, что в продакшине, под нагрузкой, все крешится прямо на старте.
Для того чтобы это сделать — вам нужно 3 гарантированно «живых» объекта — объект доступа к БД, обьект поднявший файл-подложку с диска (одноразово) и контроллер который прицепит это все к вебсерверу.
Если эдак сотную файлов с диска через readFileSync поднять получится (что вобщем-то уже не совсем корректно по идеологическим соображениям), то вот гарантированно «живой» коннекшин к базе данных уже проблема. А если еще и информация о том, что рисовать на баннерах (мета информация — куда «положить» статический текст и какой, а еще веселее — какие файлы-подложки поднять с диска) — содержится в БД — то вообще «потребуется особое техническое решение».
Другими словами dependency injection это только часть старта системы. Мне больше нравится вариант в котором после завершения dependency injection части старта системы я вижу в логах сообщение SUCCESS, что одначает, что все требуемые файлы на диске были найдены, все соединения к БД и другим системам прошли успешно. То есть приложение гарантированно работоспособное и самое последнее действие типа .listen(3493,...) произошло без приключений. Если в ходе инициализации что-то пошло «не так» то лучше чтобы приложение сразу завершилось с вменяемой ошибкой.
В вашем варианте — вы увидете некоторое SUCCESS, но у Вас может быть не верно сконфигурирована база и пары файлов на диске физически не хватает. Тут как повезет или ночью позвонят или просто приложение будет каждые 15 минут падать с трудно объяснимыми причиными.
P.S. почитайте про Spring из Java там есть много чего, чего не хватает в nodejs.Lisio
24.07.2016 18:55В своем примере вы ставите обязательным условием при старте иметь «гарантированно «живой» коннекшин к базе данных», но не учитываете, что соединение с базой может быть разорвано по множеству причин. А если все-таки учитываете, но не описали, то знаете как такую ситуацию обработать. Но если знаете как обработать, то отсутствие соединения при старте — всего лишь частный случай для такой обработки, т.е. проблемы на самом деле и нет. Либо я чего-то не понял в вашей схеме.
AxVPast
24.07.2016 19:58Нет не поняли важного. После SUCCESS в логах я на 100% уверен, что система сконфигруирована правильно как минимум на момент старта. Если я ошибся — система не стартует вообще. Это рально удобно для админов системы.
«Ваш» пример — стартуем систему. Есть ошибка в конфигурации базы данных, причем firewall съедает пакеты (то есть destination host uncharitable вы не получите никогда — поищите, кстати в доках, насколько это долго ;) ). То есть факт неработоспособности системы вы получите в виде вылета процесса по OOM :) Почему так — Ваш модуль работы с БД шибко умный и все попытки работать с базой тщательно складывает во внутреннюю очередь без контроля размера этой очереди (ждет момент, когда соединение реально появится). Это типовое поведение драйверов БД в ноде когда при инициализации вы делаете что-то типа require('redis').createClient(...).hget('НеважноЧто','неважноОткуда', function (x){…
Второй пример — пишем сетевой модуль — пару клиент и сервер. Для теста нам нжуно поднять сервер, прицепить к нему клиента, проверить, что клиент-сервер протокол работает (логично после этого и клиент и сервер опустить). Потом тест проверяет реконнект клиента (тоже старт сервера/коннект/убийство коннекта (со стороны сервера) и проверка наличия реконнекта). Следующий тест — прицепляет к серверу бизнес логику и к клинету тоже реальну реализацию. Делаем smoke тест. После этого нужно тоже все опустить (корректно). Вот и вопрос — нужен promise на инициализации обоих компонент и promise для того, чтобы все погасить.
В спринге для этого обычно заводится отдельный инициализационный файл и вся «магия» поднятия/опускания делается без наприсания кода.
Вот как обычно это выглядит в Java: http://howtodoinjava.com/spring/spring-core/spring-bean-life-cycle/
Упрощения подобной модели имеют место до определенного момента, а потом все выглядит как последний старт Челенджера.
symbix
24.07.2016 20:32Понятно. Я не считаю, что это ответственность DI, тут нужен еще один слой абстракции.
Ну и с вечной асинхронкой мне кажется более надежным CQRS/EventSourcing-подход (модные флюксы-рефлюксы это, кстати, оно и есть в упрощенной форме).
elmigranto
24.07.2016 20:14+2Что-то мне неясно, чего такое получилось в итоге. Зачем нужен JSON на 20 строчек, если можно всё это напрямую в JS писать?
ktretyak
24.07.2016 20:15В di.js есть пример для node.js, который не нужно компилить: github.com/angular/di.js/tree/master/example/node
gobwas
25.07.2016 11:02… из них самый интересный — di.js от Angular, но он мне не подошел и я решил написать свой.
=)
Если по теме – когда-то пилил тоже "свой", с асинхронной загрузкой, ленивым инстанцированием и прочими прелестями:
https://github.com/gobwas/dm.js
raveclassic
25.07.2016 21:35Не по теме DI —
использовать apply, но это не сработает, так как он взаимодействует с методами, а не конструкторами
Вообщем-то apply можно, вот так:
class Foo {
constructor(a, b) {
this.c = a + b;
}
}
const args = [2, 3]
const foo = new (Function.prototype.bind.apply(Foo, [null, ...args]));
console.log(foo.c) //5raveclassic
25.07.2016 22:07Ну вот, тэги вырезались, разметка побилась.
А вообще забавно, посмотрел, во что транспайлится spread в конструкторе — как раз вот в это.
MarcusAurelius
26.07.2016 10:31DI для классов практически не нужен. В ноде есть DL для модулей, это require, а вот простая реализация DI для модулей: https://github.com/HowProgrammingWorks/InversionOfControl/blob/master/sandboxedModule/ru/framework.js
А тут пример с декларативным описанием зависимостей модулей: https://github.com/HowProgrammingWorks/InversionOfControl/tree/master/dependencyInjection/ru
Это все только примеры использования, вся реализация уже встроена в ноду, только мало кто знает.symbix
26.07.2016 11:06Это не DI, это сервис-локатор.
MarcusAurelius
26.07.2016 11:27Сервис-локатор это когда программный компонент запрашивает у локатора ссылку на другой программного компонент (модуль, класс, интерфейс). А тут все иначе, служебный код (управляющий код, среда запуска или фреймворк) незаметно для управляемого кода (библиотеки, прикладной программы) создает контекст и внедряет в него нужные зависимости. Из контекста теперь зависимости не нужно брать через require (DL).
symbix
26.07.2016 11:44Не суть. DI — это когда конкретная реализация:
1) ничего не знает об «особом» способе получения зависимостей (и вообще о каком-то способе это делать), просто получает все, что надо, в конструктор;
2) не может сама «взять» то, чего ей не давали, не меняя конструктор.
Сервис-локатор же, напротив, это когда
1) реализация знает, что зависимости надо получать особым образом;
2) может взять из контейнера все, что там есть, в любой момент.
А уж что там будет написано, serviceLocator.get('logger') или context.logger — это не имеет значения.
При этом любой DI неизбежно содержит SL внутри. Чтобы это стало DI, надо добавить инстанциацию с constructor injection.MarcusAurelius
26.07.2016 12:05DI можно применять не только к ООП, а к классическим модулям. Вот в ноде модули это не классы, а контексты. В контексте зависимость используется не как
а какcontext.logger
logger.write('string');
иserviceLocator.get('logger')
отличаются существенно, первый получает ссылку на зависимость, а второй использует зависимость.logger.write('string');
symbix
26.07.2016 12:27А, понял вас. И посмотрел внимательнее код, извините, что сразу не сделал этого.
Тут уже начинается вкусовщина, пожалуй: мне хотелось бы управлять зависимостями на уровне конструкторов классов и интерфейсов, а не на уровне модулей.
И, кстати, разве vm.createContext — не тяжелая операция?MarcusAurelius
26.07.2016 12:53Почти ни кто на node.js не используют ООП, а управление зависимостями происходит на уровне модулей. vm.createScript тяжелая, а vm.createContext не очень тяжелая, а в этом случае скорость исполнения вообще не важна, потому, что связывание зависимостей происходит при старте приложения.
symbix
26.07.2016 13:03Ну, не знаю насчет «почти никто». Я использую (хотя я на ноде мало пишу), мои коллеги используют, в том числе TypeScript.
MarcusAurelius
26.07.2016 13:48В npm почти нет пакетов с использованием ООП и на TypeScript, а среди распространенных библиотек их вообще нет. Есть так же мнение, что ООП провалилось, http://blogerator.ru/page/oop_why-objects-have-failed Я же думаю, что его просто не умеют правильно использовать. ООП прекрасно подходит для GUI и для обертки объектов операционной системы, как то сокеты, файлы и т.д. Но вот для моделирования предметной области ООП нужно по возможности избегать и использовать структуры данных и функции, к сожалению так нельзя сделать везде, вот в Java придется все писать на ООП, а в ноде лучше писать на мультипарадигменном подходе, применять функционально и реактивное программирование, прототипное наследование, data-driven программирование, событийное и асинхронное программирование, а не лепить объекты куда угодно к месту и не к месту.
symbix
26.07.2016 14:16> для моделирования предметной области ООП нужно по возможности избегать и использовать структуры данных и функции
Это очень спорное мнение. Eric Evans с вами не согласен.
Вообще, про «провалилось» — это какой-то холивар, и обычно с основной мотивацией «не осилили». Оба подхода имеют право на жизнь. То, что перечисляют в статье — это неправильное использование ООП.MarcusAurelius
26.07.2016 14:34Согласен, в статье некомпетентно нагоняют на ООП, у меня к ООП совсем другие претензии, в основном связанные с тем, что модель предметной области в информационных системах не может иметь методов, потому, что моделируются не объекты реального мира, а лишь их информационная составляющая, например, vehicle.driveTo(address) имеет смысл только если у нас система, приводящая в действие автомобиль, но если мы работаем только с данными, и не занимаемся автоматизацией железа, то нам нужно job = { vehicle, address }. Тут vehicle и address это только только данные, практически стракты, а вот у logisticsService могут быть методы, в которые передается job, например: logisticsService.execute(job). Грубо говоря, за неимением страктов, в некоторых языках vehicle, address нужно делать классами и объектами. Для этого даже есть название «анемичные классы» и «анемичные объекты», которые не имеют поведения, только данные.
symbix
26.07.2016 22:00Что ж это за предметная область, где нет бизнес-логики, а только данные? Коли так, в программе нет никакого смысла и достаточно использовать СУБД.
Анемичная модель — это вообще не ООП, это эмуляция процедурного программирования средствами объектного языка. К ООП отношения по сути своей не имеет (от ключевого слова class программа не становится объектной). Anemic models и бездумное вынесение бизнес-логики в сервисы — это как раз антипаттерн в DDD. Очень, правда, популярный с распространением Active Record (там так проще).
Может быть, вы путаете anemic models и value objects? Value objects — это нормально, там максимум инварианты. А логика будет как минимум в aggregate root. Если последние два слова непонятны… Эванса читать советовать не буду (там много букв), лучше посоветую Domain Driven Design Quickly, она короткая, и pdf-ку раздают бесплатно.MarcusAurelius
30.07.2016 04:03Я имел в виду именно анемичные модели. Что же делать, если язык только ООП поддерживает, но программисту в задаче ООП не нужен. А бизнес-логика предметной области может находиться не в классах, если использовать не ООП. Это же не серебрянная пуля. Вот недавно с медиума статья по ООП:
https://medium.com/@cscalfani/goodbye-object-oriented-programming-a59cda4c0e53#.1ox7s5qd2
Автор резок, а мое мнение, что самое здоровое — это мультипарадигменное программирование с элементами процедурного, функционального, реактивного, асинхронного, событийного, декларативного и управляемого данными, объектного, структурного, обобщенного и метапрограммирования.symbix
30.07.2016 04:32Статью надо назвать «goodbye inheritance». Наследование — зло, я предпочитаю делегирование и композицию, а когда (иногда) наследование уместно, придерживаюсь принципа abstract or final. ООП вообще не про наследование, и Страуструповская мантра — это про С++, а не про ООП. Алан Кей вообще говорил, что когда говорил об ООП, вот то, что в С++, никак не имел ввиду :)
Бизнес-логика может находиться где угодно, но это «где угодно» должно находиться в отдельном слое domain model — в том или ином смысле. :) Иначе она будет размазана по всему коду, и при внесении изменений в бизнес-логику будет очень «весело».
Противопоставлять причин не вижу, все перечисленные вами парадигмы прекрасно уживаются вместе.
aretmy
26.07.2016 21:16Для себя использовал bottlejs. Зависимости описываются как в ангуляре, через .service, .factory, .constant и т.д. (что мне не сильно нравится). Пользовался в основном объявлением сервиса, куда передаешь конструктор класса и имена модулей, которые надо заинжектить. Мне понравилось – можно было использовать несколько инстансов одного класса как разные сервисы.
Large
Вообще di очень удобно делать через декораторы, они конечно пока stage1 и stage0, но скорей всего появятся рано или поздно в стандарте.