Чи потрібна ін'єкція залежності в NodeJS, або як боротися з…?


219

Зараз я створюю експериментальні проекти з nodejs. Я багато програмував веб-додатки Java EE разом із Spring і оцінив простоту введення залежності там.

Зараз мені цікаво: як зробити ін'єкцію залежності з вузлом? Або: мені це навіть потрібно? Чи існує концепція заміни, оскільки стиль програмування інший?

Я досі говорю про прості речі, наприклад, обмін об’єктом підключення до бази даних, але я не знайшов рішення, яке мене задовольняє.


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

Відповіді:


107

Коротше кажучи, вам не потрібен контейнер для ін'єкцій залежностей або локатор обслуговування, як це було б у 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'), людям, які використовують мій компонент, доведеться перекрити потребу використовувати свою власну бібліотеку. Натомість я можу дозволити людям передавати зворотний виклик, який завершує реалізацію реєстратора в метод компонентів "конструктор" або "init". Саме це і є метою DI.
moteutsch

4
Станом на середину 2014 року - npmjs.org/package/proxyquire робить знущання над « потребами » банальними.
arcseldon

4
Я цього не отримую, заміна вимагає в одному модулі не замінює його в іншому. Якщо я встановив функцію вимагаю в моєму тесті, а потім вимагаю, щоб модуль був протестований, заяви оператора вимагають в об'єкті, що перевіряється, не використовувати функцію, встановлену в тестовому модулі. Як це вводить залежності?
HMR

73

requireце спосіб управління залежностями в Node.js і , звичайно , це є інтуїтивно зрозумілим і ефективним, але він також має свої обмеження.

Моя порада - ознайомитись із деякими контейнерами Dependency Injection, доступними сьогодні для Node.js, щоб мати уявлення про їх плюси та мінуси. Деякі з них:

Тільки для назви кількох.

Тепер справжнє питання полягає в тому, що ви можете досягти з контейнером Node.js DI порівняно з простим require?

Плюси:

  • краща доказовість: модулі приймають свої залежності як вхідні дані
  • Інверсія управління: вирішіть, як підключити модулі, не торкаючись основного коду вашої програми.
  • налаштований алгоритм для розв'язання модулів: залежності мають "віртуальні" ідентифікатори, зазвичай вони не прив'язані до шляху до файлової системи.
  • Краща розширюваність: увімкнено IoC та "віртуальні" ідентифікатори.
  • Можливі інші модні речі:
    • Ініціалізація асинхронізації
    • Модуль управління життєвим циклом
    • Розширюваність самого контейнера DI
    • Легко реалізувати абстракції вищого рівня (наприклад, AOP)

Мінуси:

  • Відмінна від «досвіду» Node.js: не користуючись requireпевним відчуттям, ви ніби відхиляєтесь від способу мислення Вузла.
  • Зв'язок між залежністю та її реалізацією не завжди явний. Залежність може бути вирішена під час виконання та під впливом різних параметрів. Код стає важче зрозуміти та налагодити
  • Повільний час запуску
  • Зрілість (на даний момент): жодне з сучасних рішень на сьогодні не є дійсно популярним, тому не так багато навчальних посібників, жодна екосистема, не перевірена битва.
  • Деякі контейнери DI не будуть добре грати з модулями, як Browrify та Webpack.

Як і все, що стосується розробки програмного забезпечення, вибір між DI або requireзалежить від ваших вимог, складності системи та стилю програмування.


3
Як ви вважаєте, ситуація значно змінилася після '09?
Juho Vepsäläinen

13
Ви маєте на увазі з 10 днів тому? :)
Маріо

2
Nooo. 9 грудня ... Треба було знати.
Juho Vepsäläinen

4
Я "реалізував" DI, використовуючи module.exports = function (deps) {} вид шаблону. Так, це працює, але це не зовсім ідеально.
Juho Vepsäläinen

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класу. Це також значно спрощує тестування. Ви можете створити MockHornклас, який потрібно вводити в Carконструктор, щоб переконатися, що ви просто Carізолюєте клас.

Наведений вище приклад показує ручне введення залежності. Зазвичай DI робиться з рамкою (наприклад, Єдність або Нінжект у світі 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модулі. Це не дуже поширена річ, але це викликає проблеми з тестуванням.

Звичайний спосіб вирішення проблеми тестування - це проксі-запит . Завдяки динамічній природі JavaScript проксі-сервер перехоплює дзвінки, щоб вимагати та повертати будь-які створені вами заглушки / макети.

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модуль відповідає за життєві цикли та проводку. Тестування блоків досить просте, оскільки ви можете просто передати макети / заглушки до функцій. Знову ж таки, якщо це досить добре для вашої заявки, займіться цим.

Рамка Болюс Д.І.

На відміну від 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 підтримує купу чудових функцій, таких як необов'язкові залежності та тестові глобали, але є два ключові переваги, які я бачив щодо стандартного підходу 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 також класний! Зараз вони використовують внутрішній "модуль" -модуль, що набагато краще, ніж використання vm-вузла vm. Але врешті-решт це лише питання стилю. Мені подобається, що мій модуль використовує оригінальну вимогу та пізніше міняти залежності. Крім того, перемотка також дозволяє перекрити глобальний.
Йоганнес Евальд

Дуже цікаво шукав щось подібне, щоб використовувати на роботі, чи впливає цей модуль також на модулі нижче за течією?
akst

для proxyquire В описі сказано, що він використовується для тестування: "Проксі-сервери потрібні для того, щоб дозволити переважні залежності під час тестування ." не для DI, правда?
Marwen Trabelsi

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' та вводить екземпляри як аргументи експортованій функції.

Ключовим є те, що це малоінвазивно. Цей модуль є повністю придатним для використання, незалежно від самого електроліту. Це означає, що ваші тестові пристрої можуть протестувати лише тестований модуль , передаючи макетні об'єкти, не потребуючи додаткових залежностей для перемотування внутрішніх пристроїв.

Запускаючи повну програму, Електроліт входить на міжмодульний рівень, з'єднуючи речі без необхідності використання глобальних, одиночних або надмірних сантехнічних робіт.


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

В даний час використовують електроліт. Ви стверджуєте, що модулі INJECT легко експортувати за допомогою експорту ['@ потреба']. Але якщо мені доведеться заглушити один із необхідних модулів, як це досягти в електроліті. В даний час, якщо нам потрібні модулі, цього можна легко досягти. Але для електроліту це величезний мінус .... Чи є у вас приклади, коли ми можемо використовувати стерту версію модулів та передавати її динамічно під час instantiation / ioc.use з тестових випадків. Так що в основному в одиничному тесті, якщо ми можемо зробити ioc.create ('ім'я модуля'), а потім зробити ін'єкцію залежних модулів (але заглушених), було б ідеально ...
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 для незнайомих.


Дуже геніально!
Арнольд

4

Нещодавно я перевірив цю тему майже з тієї ж причини, що і в OP - більшість контурів, з якими я стикався, тимчасово переписують оператор вимагати. Я мав змішаний ступінь успіху з цим методом, і тому я закінчив використовувати наступний підхід.

У контексті експрес-програми - я загортаю 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');

Якщо жоден псевдонім не розміщений у навантажувачі - тоді він буде просто за замовчуванням для звичайної потреби. Це має дві переваги: ​​я можу поміняти місцями в будь-якій версії класу, і це усуне необхідність використання відносних імен шляхів у всій програмі (тому якщо мені потрібна власна вкладка нижче або вище поточного файлу, мені не потрібно переходити , і вимагає кешування модуля проти того ж ключа). Це також дозволяє мені вказати макети в будь-якій точці програми, а не в безпосередньому тестовому наборі.

Я лише опублікував невеликий модуль 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) підтримує безліч випадків використання:

  • Модулі ядра
  • Програмне забезпечення ядра
  • Використовуйте класи, рядкові літерали або символи як ідентифікатори залежності
  • Введення постійних значень
  • Інжекція конструкторів класу
  • Уприскування фабрик
  • Автозавод
  • Ін'єкція постачальників (async factory)
  • Обробники активації (використовуються для введення проксі)
  • Багаторазові ін’єкції
  • Позначені палітурки
  • Спеціальні декоратори тегів
  • Названі прив’язки
  • Контекстуальні прив’язки
  • Дружні винятки (наприклад, кругові залежності)

Ви можете дізнатись більше про це у 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 послуга пошти ще не існує. Використовуйте клас Посилання, щоб вказати контейнеру ввести послугу електронної пошти, коли вона ініціалізує менеджер розсилки:

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);

Якщо ви не робите OOP в JavaScript, ви можете зробити функцію init, яка все налаштовує.

Однак є ще один підхід, який ви можете скористатися, який є більш поширеним у системі, що базується на подіях, наприклад node.js. Якщо ви можете змоделювати вашій програмі лише (більшу частину часу) діяти над подіями, тоді все, що вам потрібно зробити, - це налаштувати все (що я зазвичай роблю, викликаючи функцію init) та випромінювати події з заглушки. Це робить тестування досить простим і читабельним.


Дякую за вашу відповідь, але я не повністю розумію вашу другу частину вашої відповіді.
Ерік

1

Мені завжди подобалася простота концепції IoC - "Вам не потрібно нічого знати про навколишнє середовище, вас хтось зателефонує, коли потрібно"

Але всі впровадження IoC, які я бачив, робили точно навпаки - вони захаращують код ще більше речей, ніж без нього. Отже, я створив свій власний ІОК, який працює так, як мені б хотілося, щоб він був - він залишається прихованим і невидимим 90% часу .

Він використовується у веб-структурі MonoJS http://monojs.org

Я досі говорю про прості речі, наприклад, обмін об’єктом підключення до бази даних, але я не знайшов рішення, яке мене задовольняє.

Зроблено так - зареєструйте компонент один раз у конфігурації.

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

І використовувати його де завгодно

app.db.findSomething()

Ви можете ознайомитися з повним кодом визначення компонентів (з підключенням DB та іншими компонентами) тут https://github.com/sinizinairina/mono/blob/master/mono.coffee

Це єдине місце, коли потрібно сказати IoC, що робити, після цього всі ці компоненти будуть створені та підключені автоматично, і вам більше не доведеться бачити специфічний код IoC у вашій програмі.

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


6
Незважаючи на те, що рекламується як DI, це здається набагато більше схожим на локатор обслуговування.
KyorCode

2
Чудово виглядає, соромно, це лише у кофесцензії
Рафаель П. Міранда,

1

Я думаю, що нам ще потрібна ін'єкція залежності в Nodejs, оскільки вона послаблює залежності між службами та робить додаток яснішим.

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

Відвідайте мій проект за адресою: Buncha - IoC контейнер

Дякую!



0

Я працював з .Net, PHP та Java тривалий час, тому хотів мати зручне введення залежності в NodeJS теж. Люди сказали, що вбудованого DI в NodeJS достатньо, оскільки ми можемо отримати його за допомогою модуля. Але це мене не влаштовувало. Я хотів зберегти Модуль не більше класу. Крім того, я хотів, щоб у DI була повна підтримка управління життєвим циклом модуля (однотонний модуль, перехідний модуль і т. Д.), Але з модулем "Вузол" мені довелося дуже часто писати ручний код. Нарешті, я хотів зробити полегшений тест. Тому я створив для себе ін'єкцію залежності.

Якщо ви шукаєте DI, спробуйте. Його можна знайти тут: https://github.com/robo-creative/nodejs-robo-container . Це повністю задокументовано. Він також вирішує деякі поширені проблеми з DI та способи їх вирішення способом OOP. Сподіваюся, це допомагає.


Так, ви праві, що бібліотека DI у ваших проектах важлива для гарної архітектури. Якщо ви хочете побачити приклад використання для DI, див. Readme для цього сховища також бібліотеку DI для вузла Jems DI .
Франциско Мерседес

-1

Нещодавно я створив бібліотеку з назвою схеми, яка дозволяє використовувати введення залежності з 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, які моя команда і я легко запроваджую. Тому я нещодавно побудував рамку під назвою deppie на основі цих особливостей

  • Мінімальний 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(){...}
   }
}

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

Є інші рішення (broadway, архітектор тощо), які можуть допомогти вам у цьому. хоча вони можуть робити більше, ніж ви хочете, або використовувати більше безладу.


Майже завдяки природній еволюції я закінчила те саме. Я передаю залежність у якості параметрів, і це чудово підходить для тестування.
манкі

-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.