Нужно ли вводить зависимости в NodeJS или как бороться с…?


220

В настоящее время я создаю несколько экспериментальных проектов с помощью nodejs. Я программировал множество веб-приложений на Java EE с помощью Spring и оценил простоту внедрения зависимостей.

Теперь мне любопытно: как мне сделать внедрение зависимости с узлом? Или: мне это вообще нужно? Есть ли замена концепции, потому что стиль программирования отличается?

Пока я говорю о простых вещах, таких как совместное использование объекта подключения к базе данных, но я не нашел решения, которое меня устраивало.


1
Если вы решите использовать DI, OpenTable недавно открыл для него библиотеку: github.com/opentable/spur-ioc Я использовал его (я там работаю) и могу сказать, что он довольно прост и отлично подходит для тестирования.
tybro0103

Ответы:


108

Короче говоря, вам не нужен контейнер для внедрения зависимостей или сервисный локатор, как в C # / Java. Так как Node.js использует module pattern, нет необходимости выполнять конструктор или внедрение свойства. Хотя вы все еще можете.

Самое замечательное в JS состоит в том, что вы можете изменить практически все, чтобы достичь того, чего хотите. Это очень удобно, когда дело доходит до тестирования.

Вот мой очень неубедительный надуманный пример.

MyClass.js:

var fs = require('fs');

MyClass.prototype.errorFileExists = function(dir) {
    var dirsOrFiles = fs.readdirSync(dir);
    for (var d in dirsOrFiles) {
        if (d === 'error.txt') return true;
    }
    return false;
};

MyClass.test.js:

describe('MyClass', function(){
    it('should return an error if error.txt is found in the directory', function(done){
        var mc = new MyClass();
        assert(mc.errorFileExists('/tmp/mydir')); //true
    });
});

Обратите внимание, как MyClassзависит от fsмодуля? Как упомянул @ShatyemShekhar, вы действительно можете делать конструктор или внедрение свойств, как в других языках. Но это не обязательно в Javascript.

В этом случае вы можете сделать две вещи.

Вы можете заблокировать fs.readdirSyncметод или вы можете вернуть совершенно другой модуль при вызове require.

Способ 1:

var oldmethod = fs.readdirSync;
fs.readdirSync = function(dir) { 
    return ['somefile.txt', 'error.txt', 'anotherfile.txt']; 
};

*** PERFORM TEST ***
*** RESTORE METHOD AFTER TEST ****
fs.readddirSync = oldmethod;

Способ 2:

var oldrequire = require
require = function(module) {
    if (module === 'fs') {
        return {
            readdirSync: function(dir) { 
                return ['somefile.txt', 'error.txt', 'anotherfile.txt']; 
            };
        };
    } else
        return oldrequire(module);

}

Ключ заключается в использовании возможностей Node.js и Javascript. Обратите внимание, я парень из CoffeeScript, поэтому мой синтаксис JS может быть где-то неверным. Кроме того, я не говорю, что это лучший способ, но это способ. Гуру Javascript могут быть в состоянии взаимодействовать с другими решениями.

Обновить:

Это должно ответить на ваш конкретный вопрос о подключениях к базе данных. Я бы создал отдельный модуль, чтобы вы могли инкапсулировать логику подключения к вашей базе данных. Что-то вроде этого:

MyDbConnection.js: (не забудьте выбрать лучшее имя)

var db = require('whichever_db_vendor_i_use');

module.exports.fetchConnection() = function() {
    //logic to test connection

    //do I want to connection pool?

    //do I need only one connection throughout the lifecyle of my application?

    return db.createConnection(port, host, databasename); //<--- values typically from a config file    
}

Затем любой модуль, которому требуется подключение к базе данных, просто включит ваш MyDbConnectionмодуль.

SuperCoolWebApp.js:

var dbCon = require('./lib/mydbconnection'); //wherever the file is stored

//now do something with the connection
var connection = dbCon.fetchConnection(); //mydbconnection.js is responsible for pooling, reusing, whatever your app use case is

//come TEST time of SuperCoolWebApp, you can set the require or return whatever you want, or, like I said, use an actual connection to a TEST database. 

Не следуйте этому примеру дословно. Это неудачный пример, когда вы пытаетесь сообщить, что вы используете moduleшаблон для управления своими зависимостями. Надеюсь, это поможет немного больше.


42
Это верно в отношении тестирования, но у DI есть и другие преимущества; используя DI, вы можете программировать на интерфейс, а не на реализацию.
moteutsch

3
@moteutsch Не знаю почему, так как JS не имеет понятия интерфейсов, как большинство статических языков. Все, что у вас есть, - это реализации, даже если вы хотите использовать предварительно согласованный документированный «интерфейс».
JP Richardson

16
@JPRichardson Как я могу написать компонент, который использует регистратор, не завися от какой-либо одной библиотеки? Если я require('my_logger_library'), люди, использующие мой компонент, должны будут переопределить требование использовать свою собственную библиотеку. Вместо этого я могу позволить людям передавать обратный вызов, который оборачивает реализацию регистратора в компонентный метод «конструктор» или «метод инициализации». Это цель DI.
moteutsch

4
С середины 2014 года - npmjs.org/package/proxyquire делает макетирование "требовать" зависимостей тривиальным.
arcseldon

4
Я не понимаю, замена требуют в одном модуле не заменяет его в другом. Если я установил требование для функции в моем тесте, а затем потребовал, чтобы модуль был протестирован, операторы require в тестируемом объекте не используют набор функций в тестовом модуле. Как это внедрить зависимости?
HMR

73

requireэто способ управления зависимостями в Node.js и , конечно , это является интуитивно понятным и эффективным, но он также имеет свои ограничения.

Я советую взглянуть на некоторые из доступных сегодня контейнеров Dependency Injection для Node.js, чтобы понять их плюсы и минусы. Некоторые из них:

Просто назвать несколько.

Теперь реальный вопрос: чего вы можете достичь с помощью контейнера Node.js DI по сравнению с простым require?

Плюсы:

  • лучшая тестируемость: модули принимают в качестве входных данных свои зависимости
  • Инверсия управления: решите, как подключить ваши модули, не касаясь основного кода вашего приложения.
  • настраиваемый алгоритм разрешения модулей: зависимости имеют «виртуальные» идентификаторы, обычно они не привязаны к пути в файловой системе.
  • Лучшая расширяемость: обеспечивается IoC и «виртуальными» идентификаторами.
  • Возможны другие модные вещи:
    • Асинхронная инициализация
    • Управление жизненным циклом модуля
    • Расширяемость самого контейнера DI
    • Может легко реализовать абстракции более высокого уровня (например, AOP)

Минусы:

  • В отличие от «опыта» Node.js: отсутствие использования requireопределенно означает, что вы отклоняетесь от мышления Node.
  • Связь между зависимостью и ее реализацией не всегда является явной. Зависимость может быть разрешена во время выполнения и зависит от различных параметров. Код становится более сложным для понимания и отладки
  • Медленное время запуска
  • Срок погашение (на данный момент): ни один из существующих решений является очень популярным в настоящее время, так что не так много учебников, ни экосистемы, а не боя испытания.
  • Некоторые DI-контейнеры не будут хорошо работать с пакетами модулей, такими как Browserify и Webpack.

Как и во всем, что связано с разработкой программного обеспечения, выбор между DI или requireзависит от ваших требований, сложности вашей системы и вашего стиля программирования.


3
Как вы думаете, ситуация значительно изменилась с 2009 года?
Юхо Вепсяляйнен

13
Вы имеете в виду с 10 дней назад? :)
Марио

2
Нееет. 9 декабря ... Должен был знать.
Юхо Вепсяляйнен

4
Я "реализовал" DI, используя шаблон вида module.exports = function (deps) {}. Да, это работает, но это не совсем идеально.
Юхо Вепсяляйнен

3
Модули принимают свои зависимости как входные данные, и Зависимости не кажутся мне явным противоречием.
Антон Рудешко

53

Я знаю, что эта тема довольно старая на данный момент, но я решила, что подумаю об этом. TL; DR заключается в том, что из-за нетипизированной, динамической природы JavaScript вы действительно можете сделать довольно много, не прибегая к шаблону внедрения зависимостей (DI) или используя инфраструктуру DI. Однако по мере того, как приложение становится все больше и сложнее, DI определенно может помочь в поддержке вашего кода.

DI в C #

Чтобы понять, почему DI не так важен в JavaScript, полезно взглянуть на строго типизированный язык, такой как C #. (Приношу извинения тем, кто не знает C #, но за ним должно быть достаточно легко следить.) Скажем, у нас есть приложение, которое описывает автомобиль и его гудок. Вы бы определили два класса:

class Horn
{
    public void Honk()
    {
        Console.WriteLine("beep!");
    }
}

class Car
{
    private Horn horn;

    public Car()
    {
        this.horn = new Horn();
    }

    public void HonkHorn()
    {
        this.horn.Honk();
    }
}

class Program
{
    static void Main()
    {
        var car = new Car();
        car.HonkHorn();
    }
}

Есть несколько проблем с написанием кода таким способом.

  1. CarКласс тесно связан с конкретной реализацией рога в Hornклассе. Если мы хотим изменить тип звукового сигнала, используемого автомобилем, мы должны изменить Carкласс, даже если его использование не изменится. Это также затрудняет тестирование, потому что мы не можем тестировать Carкласс изолированно от его зависимости, Hornкласса.
  2. CarКласс отвечает за жизненный цикл этого Hornкласса. В простом примере, подобном этому, это не является большой проблемой, но в реальных приложениях зависимости будут иметь зависимости, которые будут иметь зависимости и т. Д. CarКласс должен отвечать за создание всего дерева своих зависимостей. Это не только сложно и повторяется, но и нарушает «единственную ответственность» класса. Следует сосредоточиться на том, чтобы быть автомобилем, а не создавать экземпляры.
  3. Невозможно повторно использовать одни и те же экземпляры зависимостей. Опять же, это не важно в этом игрушечном приложении, но рассмотрим соединение с базой данных. Обычно у вас есть один экземпляр, который используется в вашем приложении.

Теперь давайте сделаем рефакторинг для использования шаблона внедрения зависимостей.

interface IHorn
{
    void Honk();
}

class Horn : IHorn
{
    public void Honk()
    {
        Console.WriteLine("beep!");
    }
}

class Car
{
    private IHorn horn;

    public Car(IHorn horn)
    {
        this.horn = horn;
    }

    public void HonkHorn()
    {
        this.horn.Honk();
    }
}

class Program
{
    static void Main()
    {
        var horn = new Horn();
        var car = new Car(horn);
        car.HonkHorn();
    }
}

Мы сделали две ключевые вещи здесь. Во-первых, мы представили интерфейс, который Hornреализует наш класс. Это позволяет нам кодировать Carкласс для интерфейса вместо конкретной реализации. Теперь код может принимать все, что реализует IHorn. Во-вторых, мы сняли инстанцирование рога Carи вместо этого передали его. Это решает описанные выше проблемы и оставляет основной функции приложения управлять конкретными экземплярами и их жизненными циклами.

Это означает, что это может привести к появлению нового типа рупора для автомобиля, не касаясь Carкласса:

class FrenchHorn : IHorn
{
    public void Honk()
    {
        Console.WriteLine("le beep!");
    }
}

FrenchHornВместо этого main может просто внедрить экземпляр класса. Это также значительно упрощает тестирование. Вы можете создать MockHornкласс для внедрения в Carконструктор, чтобы убедиться, что вы тестируете только Carкласс в изоляции.

В приведенном выше примере показано внедрение зависимостей вручную. Обычно DI выполняется с помощью фреймворка (например, Unity или Ninject в мире C #). Эти фреймворки будут выполнять всю проводку зависимостей, обходя график зависимостей и создавая экземпляры по мере необходимости.

Стандартный путь Node.js

Теперь давайте посмотрим на тот же пример в Node.js. Мы, вероятно, разбили бы наш код на 3 модуля:

// horn.js
module.exports = {
    honk: function () {
        console.log("beep!");
    }
};

// car.js
var horn = require("./horn");
module.exports = {
    honkHorn: function () {
        horn.honk();
    }
};

// index.js
var car = require("./car");
car.honkHorn();

Поскольку JavaScript нетипизирован, мы не имеем такой же тесной связи, как раньше. Нет необходимости в интерфейсах (и они не существуют), так как carмодуль просто попытается вызвать honkметод для того, что hornмодуль экспортирует.

Кроме того, поскольку Node requireкэширует все, модули по сути являются синглетонами, которые хранятся в контейнере. Любой другой модуль, который выполняет requireнад hornмодулем, получит точно такой же экземпляр. Это делает совместное использование одноэлементных объектов, таких как соединения с базой данных, очень простым.

Теперь все еще существует проблема, заключающаяся в том, что carмодуль отвечает за выбор своей собственной зависимости horn. Если вы хотите, чтобы автомобиль использовал другой модуль для своего клаксона, вам нужно изменить requireоператор в carмодуле. Это не очень распространенная вещь, но она вызывает проблемы с тестированием.

Обычно люди справляются с проблемой тестирования с помощью proxyquire . Вследствие динамической природы JavaScript, proxyquire перехватывает вызовы require и возвращает любые заглушки / насмешки, которые вы предоставляете.

var proxyquire = require('proxyquire');
var hornStub = {
    honk: function () {
        console.log("test beep!");
    }
};

var car = proxyquire('./car', { './horn': hornStub });

// Now make test assertions on car...

Этого более чем достаточно для большинства приложений. Если это работает для вашего приложения, то иди с ним. Тем не менее, по моему опыту, поскольку приложения становятся все больше и сложнее, поддерживать такой код становится все труднее.

DI в JavaScript

Node.js очень гибкий. Если вы не удовлетворены описанным выше методом, вы можете написать свои модули, используя шаблон внедрения зависимостей. В этом шаблоне каждый модуль экспортирует фабричную функцию (или конструктор класса).

// horn.js
module.exports = function () {
    return {
        honk: function () {
            console.log("beep!");
        }
    };
};

// car.js
module.exports = function (horn) {
    return {
        honkHorn: function () {
            horn.honk();
        }
    };
};

// index.js
var horn = require("./horn")();
var car = require("./car")(horn);
car.honkHorn();

Это очень похоже на метод C # ранее в том, что index.jsмодуль отвечает за жизненные циклы экземпляров и проводку. Модульное тестирование довольно простое, так как вы можете просто передать макеты / заглушки функциям. Опять же, если это достаточно хорошо для вашего приложения, используйте его.

Bolus DI Framework

В отличие от C #, не существует установленных стандартных структур DI, которые бы помогли вам в управлении зависимостями. В реестре npm есть несколько платформ, но ни одна из них не получила широкого распространения. Многие из этих вариантов уже упоминались в других ответах.

Я не был особенно доволен ни одним из доступных вариантов, поэтому я написал свой болюс . Bolus предназначен для работы с кодом, написанным в стиле DI выше, и старается быть очень СУХИМЫМ и очень простым. Используя тот же самый car.jsи horn.jsмодулей выше, вы можете переписать index.jsмодуль с болюса , как:

// index.js
var Injector = require("bolus");
var injector = new Injector();
injector.registerPath("**/*.js");

var car = injector.resolve("car");
car.honkHorn();

Основная идея заключается в том, что вы создаете инжектор. Вы регистрируете все свои модули в инжекторе. Тогда вы просто решаете, что вам нужно. Bolus будет обходить граф зависимостей и создавать и вводить зависимости по мере необходимости. В таком игрушечном примере вы не сильно экономите, но в больших приложениях со сложными деревьями зависимостей экономия огромна.

Bolus поддерживает множество полезных функций, таких как необязательные зависимости и тестовые глобалы, но я вижу два ключевых преимущества по сравнению со стандартным подходом Node.js. Во-первых, если у вас много похожих приложений, вы можете создать частный модуль npm для своей базы, который создает инжектор и регистрирует на нем полезные объекты. Тогда ваши конкретные приложения могут добавлять, переопределять и разрешать по мере необходимости так же, как AngularJSинжектор работает. Во-вторых, вы можете использовать болюс для управления различными контекстами зависимостей. Например, вы можете использовать промежуточное ПО для создания дочернего инжектора для каждого запроса, регистрации идентификатора пользователя, идентификатора сеанса, регистратора и т. Д. В инжекторе вместе с любыми модулями, в зависимости от них. Затем решите, что вам нужно для обслуживания запросов. Это дает вам экземпляры ваших модулей для каждого запроса и предотвращает необходимость передавать регистратор и т. Д. При каждом вызове функции модуля.


1
Также верно, что есть альтернатива, proxyquireнапример, sinonкоторая позволяет вам делать очень лаконичные макеты, например, let readFileStub = sinon.stub(fs, 'readFile').yields(new Error('something went wrong'));и затем последующие вызовы fs.readFileбудут возвращать ошибку, пока вы не вернете заглушку через readFileStub.restore(). Лично я нахожу DI сомнительным использованием, потому что я чувствую, что это почти требует использования классов / объектов, что является сомнительным предположением, учитывая функциональные предпочтения javascript.
Кевин

Спасибо за этот хороший + подробный ответ. Я почти пропустил это, поскольку я сначала прочитал заголовок DI в C # .
Константин А.

1
Отличный ответ. Я задаюсь вопросом , что ваши мысли в 2019. Для крупных проектов, как вопрос личных предпочтений, которые вы предпочитаете - DI / IoC в узле, или просто гася / насмешливый с jest, rewire, proxyquireи т.д.? Спасибо.
Джейми Коркхилл

Отличный сбалансированный ответ! Спасибо.
Джонни Ошика

36

Я также написал модуль для этого, он называется rewire . Просто используйте, npm install rewireа затем:

var rewire = require("rewire"),
    myModule = rewire("./path/to/myModule.js"); // exactly like require()

// Your module will now export a special setter and getter for private variables.
myModule.__set__("myPrivateVar", 123);
myModule.__get__("myPrivateVar"); // = 123


// This allows you to mock almost everything within the module e.g. the fs-module.
// Just pass the variable name as first parameter and your mock as second.
myModule.__set__("fs", {
    readFile: function (path, encoding, cb) {
        cb(null, "Success!");
    }
});
myModule.readSomethingFromFileSystem(function (err, data) {
    console.log(data); // = Success!
});

Меня вдохновил инъектор Натана Макиннеса, но я использовал другой подход. Я не использую vmдля оценки тестового модуля, на самом деле я использую собственное требование узла. Таким образом , ваш модуль ведет себя так же , как с помощью require()( за исключением ваших изменений). Также полностью поддерживается отладка.


7
С середины 2014 года - npmjs.org/package/proxyquire делает макетирование "требовать" зависимостей тривиальным.
arcseldon

Proxyquire это тоже круто! Теперь они используют внутренний модуль «module», что намного лучше, чем использование узла vm. Но ведь это просто вопрос стиля. Мне нравится, что мой модуль использует исходное требование и позже поменяет зависимости. Кроме того, rewire также позволяет перекрывать глобалы.
Йоханнес Эвальд

Очень интересно было искать что-то подобное для использования на работе, влияет ли этот модуль также на последующие модули?
AKST

for proxyquire В описании, которое используется для тестирования, сказано: «Projies nodejs требуется, чтобы разрешить переопределение зависимостей во время тестирования» . не для DI, верно?
Марвен Трабелси

17

Я построил Электролит только для этой цели. Другие решения для инъекций зависимостей были слишком агрессивными для моих вкусов, и возиться с глобальными requireпроблемами - моя особая претензия.

Электролит включает в себя модули, особенно те, которые экспортируют функцию «настройки», как вы видите в промежуточном программном обеспечении Connect / Express. По сути, эти типы модулей являются просто фабриками для некоторого объекта, который они возвращают.

Например, модуль, который создает соединение с базой данных:

var mysql = require('mysql');

exports = module.exports = function(settings) {
  var connection = mysql.createConnection({
    host: settings.dbHost,
    port: settings.dbPort
  });

  connection.connect(function(err) {
    if (err) { throw err; }
  });

  return connection;
}

exports['@singleton'] = true;
exports['@require'] = [ 'settings' ];

Внизу вы видите аннотации , дополнительный бит метаданных, которые Electrolyte использует для создания и внедрения зависимостей, автоматически соединяя компоненты вашего приложения.

Чтобы создать соединение с базой данных:

var db = electrolyte.create('database');

Электролит транзитивно пересекает @requireзависимости d и вводит экземпляры в качестве аргументов экспортируемой функции.

Ключ в том, что это минимально инвазивно. Этот модуль полностью пригоден для использования независимо от самого электролита. Это означает, что ваши модульные тесты могут тестировать только тестируемый модуль , передавая фиктивные объекты без необходимости в дополнительных зависимостях для перепрограммирования внутренних компонентов.

При запуске полного приложения, Electrolyte входит на межмодульном уровне, соединяя все вместе без необходимости глобализаций, синглетонов или чрезмерной сантехники.


1
Не могли бы вы уточнить, что происходит в коде, который вы разместили при вызове connect()бросков? Хотя я не знаком с MySql API для Node, я ожидаю, что этот вызов будет асинхронным, поэтому иллюстрация не совсем понятна.
Андрей Агибалов

В настоящее время используется электролит. Вы утверждаете, что модули INJECT легко экспортировать через экспорт ['@ require']. Но если мне нужно заглушить один из необходимых модулей, как это достигается в электролите. В настоящее время, если нам требуются модули, это может быть легко достигнуто. Но для электролита это огромный минус .... У вас есть примеры, когда мы можем использовать заглушенную версию модулей и передавать ее динамически во время instantiation / ioc.use из тестовых случаев. Таким образом, в основном в модульном тестировании, если мы можем сделать ioc.create ('modulename'), а затем сделать инъекцию зависимых модулей (но заглушенных), было бы идеально ...
user1102171

1
Вы не будете звонить ioc.createс юнит-теста. Модульное тестирование должно проверять только тестируемый модуль и не содержать других зависимостей, включая электролит. Следуя этому совету, вы бы поступилиobjToTest = require('modulename')(mockObj1, mockObj2);
Джаред Хэнсон

8

Я посмотрел на это сам. Мне не нравится вводить волшебные зависимости, использует библиотеки, которые предоставляют механизмы для захвата импорта моего модуля. Вместо этого я придумал «руководство по проектированию» для своей команды, чтобы довольно явно указать, какие зависимости можно смоделировать, введя экспорт фабричных функций в мои модули.

Я широко использую функции ES6 для параметров и деструктуризации, чтобы избежать некоторых шаблонов и обеспечить именованный механизм переопределения зависимостей.

Вот пример:

import foo from './utils/foo';
import bob from './utils/bob';

// We export a factory which accepts our dependencies.
export const factory = (dependencies = {}) => {
  const {
    // The 'bob' dependency.  We default to the standard 'bob' imp if not provided.
    $bob = bob, 
    // Instead of exposing the whole 'foo' api, we only provide a mechanism
    // with which to override the specific part of foo we care about.
    $doSomething = foo.doSomething // defaults to standard imp if none provided.
  } = dependencies;  

  return function bar() {
    return $bob($doSomething());
  }
}

// The default implementation, which would end up using default deps.
export default factory();

И вот пример его использования

import { factory } from './bar';

const underTest = factory({ $bob: () => 'BOB!' }); // only override bob!
const result = underTest();

Извините синтаксис ES6 для тех, кто не знаком с ним.


Очень гениально!
Arnold

4

Я недавно проверил этот поток по той же причине, что и OP - большинство библиотек, с которыми я столкнулся, временно переписывают оператор require. Я имел смешанные степени успеха с этим методом, и поэтому я использовал следующий подход.

В контексте экспресс-приложения - я оборачиваю app.js в файл bootstrap.js:

var path = require('path');
var myapp = require('./app.js');

var loader = require('./server/services/loader.js');

// give the loader the root directory
// and an object mapping module names 
// to paths relative to that root
loader.init(path.normalize(__dirname), require('./server/config/loader.js')); 

myapp.start();

Карта объектов, переданная загрузчику, выглядит следующим образом:

// live loader config
module.exports = {
    'dataBaseService': '/lib/dataBaseService.js'
}

// test loader config
module.exports = {
    'dataBaseService': '/mocks/dataBaseService.js'
    'otherService' : {other: 'service'} // takes objects too...
};

Тогда вместо прямого звонка требуется ...

var myDatabaseService = loader.load('dataBaseService');

Если в загрузчике нет псевдонима, то по умолчанию он будет обычным требованием. Это имеет два преимущества: я могу поменяться любой версией класса, и это избавит от необходимости использовать относительные имена путей во всем приложении (поэтому, если мне нужна пользовательская библиотека ниже или выше текущего файла, мне не нужно проходить , и require будет кешировать модуль против того же ключа). Это также позволяет мне указывать макеты в любой точке приложения, а не в наборе немедленных тестов.

Я только что опубликовал небольшой модуль npm для удобства:

https://npmjs.org/package/nodejs-simple-loader


3

Реальность такова, что вы можете протестировать ваш node.js без контейнера IoC, потому что JavaScript - это действительно динамический язык программирования, и вы можете изменять практически все во время выполнения.

Учтите следующее:

import UserRepository from "./dal/user_repository";

class UserController {
    constructor() {
        this._repository = new UserRepository();
    }
    getUsers() {
        this._repository.getAll();
    }
}

export default UserController;

Таким образом, вы можете переопределить связь между компонентами во время выполнения. Мне нравится думать, что мы должны стремиться отделить наши модули JavaScript.

Единственный способ добиться реального разделения - удалить ссылку на UserRepository:

class UserController {
    constructor(userRepository) {
        this._repository = userRepository;
    }
    getUsers() {
        this._repository.getAll();
    }
}

export default UserController;

Это означает, что где-то еще вам нужно будет сделать композицию объекта:

import UserRepository from "./dal/user_repository";
import UserController from "./dal/user_controller";

export default new UserController(new UserRepository());

Мне нравится идея делегирования композиции объекта в контейнер IoC. Вы можете узнать больше об этой идее в статье Текущее состояние инверсии зависимостей в JavaScript . Статья пытается разоблачить некоторые «мифы о контейнере JavaScript IoC»:

Миф 1: в JavaScript нет места для контейнеров IoC

Миф 2: нам не нужны контейнеры IoC, у нас уже есть загрузчики модулей!

Миф 3: инверсия зависимостей === введение зависимостей

Если вам также нравится идея использования контейнера IoC, вы можете взглянуть на InversifyJS. Последний выпуск (2.0.0) поддерживает множество вариантов использования:

  • Модули ядра
  • Ядро промежуточного программного обеспечения
  • Используйте классы, строковые литералы или символы в качестве идентификаторов зависимости
  • Инъекция постоянных значений
  • Инъекция конструкторов классов
  • Инъекция заводов
  • Авто завод
  • Инъекция провайдеров (асинхронная фабрика)
  • Обработчики активации (используются для внедрения прокси)
  • Мульти инъекции
  • Помеченные привязки
  • Пользовательские теги декораторы
  • Именованные привязки
  • Контекстные привязки
  • Дружественные исключения (например, круговые зависимости)

Вы можете узнать больше об этом в InversifyJS .


2

Для ES6 я разработал этот контейнер https://github.com/zazoomauro/node-dependency-injection

import {ContainerBuilder} from 'node-dependency-injection'

let container = new ContainerBuilder()
container.register('mailer', 'Mailer')

Тогда вы можете установить, например, выбор транспорта в контейнере:

import {ContainerBuilder} from 'node-dependency-injection'

let container = new ContainerBuilder()
container
  .register('mailer', 'Mailer')
  .addArgument('sendmail')

Этот класс теперь гораздо более гибкий, поскольку вы разделили выбор транспорта из реализации и в контейнер.

Теперь, когда почтовый сервис находится в контейнере, вы можете внедрить его как зависимость от других классов. Если у вас есть класс NewsletterManager, подобный этому:

class NewsletterManager {
    construct (mailer, fs) {
        this._mailer = mailer
        this._fs = fs
    }
}

export default NewsletterManager

При определении службы newsletter_manager служба почтовой рассылки еще не существует. Используйте класс Reference, чтобы указать контейнеру внедрить службу почтовой программы при инициализации менеджера новостной рассылки:

import {ContainerBuilder, Reference, PackageReference} from 'node-dependency-injection'
import Mailer from './Mailer'
import NewsletterManager from './NewsletterManager'

let container = new ContainerBuilder()

container
  .register('mailer', Mailer)
  .addArgument('sendmail')

container
  .register('newsletter_manager', NewsletterManager)
  .addArgument(new Reference('mailer'))
  .addArgument(new PackageReference('fs-extra'))

Вы также можете настроить контейнер с помощью файлов конфигурации, таких как файлы Yaml, Json или JS

Служебный контейнер может быть скомпилирован по разным причинам. Эти причины включают проверку любых потенциальных проблем, таких как циклические ссылки и повышение эффективности контейнера.

container.compile()

1

Это зависит от дизайна вашего приложения. Очевидно, что вы можете сделать Java-инъекцию, где вы создаете объект класса с зависимостью, передаваемой в конструктор, как это.

function Cache(store) {
   this._store = store;
}

var cache = new Cache(mysqlStore);

Если вы не делаете ООП в javascript, вы можете сделать функцию инициализации, которая все настраивает.

Тем не менее, есть другой подход, который вы можете использовать, который более распространен в системе, основанной на событиях, такой как node.js. Если вы можете смоделировать ваше приложение только для того, чтобы (в большинстве случаев) воздействовать на события, тогда все, что вам нужно сделать, это настроить все (что я обычно делаю, вызывая функцию init) и генерировать события из заглушки. Это делает тестирование довольно простым и читабельным.


Спасибо за ваш ответ, но я не совсем понимаю вашу вторую часть вашего ответа.
Эрик

1

Мне всегда нравилась простота концепции IoC: «Вам не нужно ничего знать об окружающей среде, вам будет кто-то позвонить, когда потребуется»

Но все реализации IoC, которые я видел, делали с точностью до наоборот - они загромождали код даже большим количеством вещей, чем без него. Итак, я создал свой собственный IoC, который работает так, как мне бы хотелось - он остается скрытым и невидимым 90% времени .

Он используется в веб-фреймворке MonoJS http://monojs.org

Пока я говорю о простых вещах, таких как совместное использование объекта подключения к базе данных, но я не нашел решения, которое меня устраивало.

Это сделано так - зарегистрируйте компонент один раз в конфиге.

app.register 'db', -> 
  require('mongodb').connect config.dbPath

И использовать его где угодно

app.db.findSomething()

Вы можете увидеть полный код определения компонента (с подключением к БД и другими компонентами) здесь https://github.com/sinizinairina/mono/blob/master/mono.coffee.

Это единственное место, когда вы должны указать IoC, что делать, после этого все эти компоненты будут созданы и подключены автоматически, и вам больше не нужно будет видеть специфичный для IoC код в вашем приложении.

Сам IoC https://github.com/alexeypetrushin/miconjs


6
Хотя рекламируется как DI, это больше похоже на поиск сервисов.
KyorCode

2
Выглядит великолепно, позор, что это только в coffescript
Рафаэль П. Миранда

1

Я думаю, что нам все еще нужно внедрение зависимостей в Nodejs, потому что оно ослабляет зависимости между сервисами и делает приложение более понятным.

Вдохновленный Spring Framework , я также реализую свой собственный модуль для поддержки внедрения зависимостей в Nodejs. Мой модуль также может обнаруживать code changesи auto reloadобслуживать без перезагрузки приложения.

Посетите мой проект по адресу: Бунча - контейнер IoC

Спасибо!



0

Я работал с .Net, PHP и Java в течение долгого времени, поэтому я хотел иметь удобное внедрение зависимостей и в NodeJS. Люди сказали, что встроенного DI в NodeJS достаточно, поскольку мы можем получить его с помощью Module. Но это не удовлетворило меня хорошо. Я хотел сохранить модуль не более, чем класс. Кроме того, я хотел, чтобы DI имел полную поддержку управления жизненным циклом модулей (одноэлементный модуль, переходный модуль и т. Д.), Но с модулем Node мне приходилось очень часто писать ручной код. Наконец, я хотел облегчить юнит тест. Вот почему я создал инъекцию зависимости для себя.

Если вы ищете DI, попробуйте. Его можно найти здесь: https://github.com/robo-creative/nodejs-robo-container . Это полностью задокументировано. Также рассматриваются некоторые распространенные проблемы с DI и способы их решения ООП. Надеюсь, поможет.


Да, вы правы, библиотека DI в ваших проектах важна для хорошей архитектуры. Если вы хотите увидеть пример использования DI, ознакомьтесь с файлом readme для этого хранилища, а также библиотекой DI для узла Jems DI .
Франсиско Мерседес

-1

Недавно я создал библиотеку с именем circuitbox, которая позволяет вам использовать внедрение зависимостей с помощью node.js. Он действительно внедряет зависимости по сравнению со многими библиотеками на основе поиска зависимостей, которые я видел. Circuitbox также поддерживает процедуры асинхронного создания и инициализации. Ниже приведен пример:

Предположим, что следующий код находится в файле с именем consoleMessagePrinter.js

'use strict';

// Our console message printer
// deps is injected by circuitbox with the dependencies
function ConsoleMessagePrinter(deps) {
  return {
    print: function () {
      console.log(deps.messageSource.message());
    }
  };
}

module.exports = ConsoleMessagePrinter;

Предположим, что следующее находится в файле main.js

'use strict';

// our simple message source
// deps is injected by circuitbox with the dependencies
var simpleMessageSource = function (deps) {
  return {
    message: function () {
      return deps.message;
    }
  };
};

// require circuitbox
var circuitbox = require('../lib');

// create a circuitbox
circuitbox.create({
  modules: [
    function (registry) {
      // the message to be used
      registry.for('message').use('This is the message');

      // define the message source
      registry.for('messageSource').use(simpleMessageSource)
        .dependsOn('message');

      // define the message printer - does a module.require internally
      registry.for('messagePrinter').requires('./consoleMessagePrinter')
        .dependsOn('messageSource');
    }
  ]
}).done(function (cbx) {

  // get the message printer and print a message
  cbx.get('messagePrinter').done(function (printer) {
    printer.print();
  }, function (err) {
    console.log('Could not recieve a printer');
    return;
  });

}, function (err) {
  console.log('Could not create circuitbox');
});

Circuitbox позволяет вам определять ваши компоненты и объявлять их зависимости как модули. После инициализации он позволяет вам получить компонент. Circuitbox автоматически внедряет все компоненты, необходимые целевому компоненту, и предоставляет его вам для использования.

Проект в альфа-версии. Ваши комментарии, идеи и отзывы приветствуются.

Надеюсь, поможет!


-1

Я думаю, что другие посты проделали большую работу в аргументе для использования DI. Для меня причины

  1. Введите зависимости, не зная их пути. Это означает, что если вы меняете расположение модуля на диске или меняете его другим, вам не нужно трогать каждый файл, который от него зависит.

  2. Это позволяет намного проще смоделировать зависимости для тестирования без необходимости переопределения глобальной requireфункции таким образом, чтобы она работала без проблем.

  3. Это поможет вам организовать и обосновать ваше приложение как слабо связанные модули.

Но мне было очень трудно найти структуру DI, которую я и моя команда легко могли бы принять. Поэтому я недавно построил фреймворк под названием Деппи на основе этих функций

  • Минимальный API, который можно освоить за несколько минут
  • Никакого дополнительного кода / конфигурации / аннотаций не требуется
  • Прямое сопоставление один на один с requireмодулями
  • Может быть частично адаптирован для работы с существующим кодом

-1

Это должно быть гибким и простым, как это:

var MyClass1 = function () {}
var MyClass2 = function (myService1) {
    // myService1.should.be.instanceof(MyClass1); 
}


container.register('myService1', MyClass1);
container.register('myService2', MyClass2, ['myService1']);

Я написал статью о внедрении зависимостей в node.js.

Я надеюсь, что это может помочь вам в этом.


-1

Node.js требует DI столько же, сколько любая другая платформа. Если вы создаете что-то большое, DI облегчит моделирование зависимостей вашего кода и тщательное тестирование кода.

Например, ваши модули уровня базы данных не должны просто потребоваться в ваших модулях бизнес-кода, потому что при модульном тестировании этих модулей бизнес-кода происходит загрузка и подключение к базе данных.

Одним из решений было бы передать зависимости как параметры модуля:

module.exports = function (dep1, dep2) {
// private methods

   return {
    // public methods
       test: function(){...}
   }
}

Таким образом, зависимости могут быть легко и естественно смоделированы, и вы можете сосредоточиться на тестировании своего кода без использования какой-либо хитрой сторонней библиотеки.

Существуют и другие решения (бродвей, архитектор и т. Д.), Которые могут помочь вам в этом. хотя они могут делать больше, чем вы хотите, или использовать больше беспорядка.


Почти через естественную эволюцию я закончил тем же. Я передаю зависимость в качестве параметров, и она отлично работает для тестирования.
Манки

-1

Я разработал библиотеку, которая обрабатывает внедрение зависимостей простым способом, который уменьшает стандартный код. Каждый модуль определяется уникальным именем и функцией контроллера. Параметры контроллера отражают зависимости модуля.

Узнайте больше на KlarkJS

Краткий пример:

KlarkModule(module, 'myModuleName1', function($nodeModule1, myModuleName2) {
    return {
        log: function() { console.log('Hello from module myModuleName1') }
    };
});
  • myModuleName1 это имя модуля.
  • $nodeModule1это внешняя библиотека из node_module. Имя разрешается до node-module1. Префикс $указывает, что это внешний модуль.
  • myModuleName2 это имя внутреннего модуля.
  • Возвращаемое значение контроллера используется из других внутренних модулей, когда они определяют параметр myModuleName1.

-1

Я обнаружил этот вопрос, отвечая на вопрос в моем собственном модуле DI, спрашивающий, зачем когда-либо понадобится система DI для программирования NodeJS.

Ответ был явно склонен к тем, которые приведены в этой теме: это зависит. Для обоих подходов есть компромиссы, и чтение ответов на этот вопрос дает им хорошую форму.

Таким образом, реальный ответ на этот вопрос должен состоять в том, что в некоторых ситуациях вы будете использовать систему DI, в других - нет.

Тем не менее, вы, как разработчик, не хотите повторяться и повторно использовать свои услуги в различных приложениях.

Это означает, что мы должны написать сервисы, которые готовы к использованию в системе DI, но не привязаны к библиотекам DI. Для меня это означает, что мы должны написать такие сервисы:

module.exports = initDBService;

// Tells any DI lib what it expects to find in it context object
// The $inject prop is the de facto standard for DI imo 
initDBService.$inject = ['ENV'];

// Note the context object, imo, a DI tool should bring
// services in a single context object
function initDBService({ ENV }) {
/// actual service code
}

Таким образом, ваш сервис работает независимо от того, используете ли вы его с инструментом DI или без него.


-1

TypeDI является самым сладким из всех упомянутых здесь, посмотрите этот код в TypeDI

import "reflect-metadata";
import {Service, Container} from "typedi";

@Service()
class SomeClass {

    someMethod() {
    }

}

let someClass = Container.get(SomeClass);
someClass.someMethod();

Посмотрите и этот код:

import {Container, Service, Inject} from "typedi";

// somewhere in your global app parameters
Container.set("authorization-token", "RVT9rVjSVN");

@Service()
class UserRepository {

    @Inject("authorization-token")
    authorizationToken: string;

}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.