Нужно ли вводить зависимости в NodeJS или как бороться с ...? - PullRequest
188 голосов
/ 12 февраля 2012

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

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

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

Ответы [ 21 ]

96 голосов
/ 13 февраля 2012

Короче говоря, вам не нужен контейнер внедрения зависимостей или сервисный локатор, как в 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 для управления своими зависимостями.Надеюсь, это поможет немного больше.

68 голосов
/ 09 декабря 2013

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

Мой совет - взглянуть на некоторые из доступных сегодня контейнеров для инъекций зависимости для Node.js, чтобы понять их плюсы и минусы.Вот некоторые из них:

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

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

Плюсы:

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

Минусы:

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

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

40 голосов
/ 20 февраля 2016

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

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

Основное может просто внедрить экземпляр класса FrenchHorn.Это также значительно упрощает тестирование.Вы можете создать класс 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's 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 . Bolus предназначен для работы с кодом, написанным в стиле DI выше, и старается быть очень DRY и очень простым. Используя те же самые модули 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 инжектор. Во-вторых, вы можете использовать болюс для управления различными контекстами зависимостей. Например, вы можете использовать промежуточное ПО для создания дочернего инжектора для каждого запроса, регистрации идентификатора пользователя, идентификатора сеанса, регистратора и т. Д. В инжекторе вместе с любыми модулями, в зависимости от них. Затем решите, что вам нужно для обслуживания запросов. Это дает вам экземпляры ваших модулей для каждого запроса и предотвращает необходимость передавать регистратор и т. Д. При каждом вызове функции модуля.

37 голосов
/ 04 июня 2012

Я также написал модуль для этого, он называется 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. Таким образом, ваш модуль ведет себя так же, как при использовании require() (кроме ваших модификаций). Также полностью поддерживается отладка.

17 голосов
/ 26 ноября 2013

Я построил Электролит именно для этой цели.Другие решения для инъекций зависимостей были слишком агрессивными для моего вкуса, и возмущение глобальным 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 и вставляет экземпляры в качестве аргументов экспортируемой функции.

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

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

8 голосов
/ 08 февраля 2016

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

Я широко использую функции 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 для тех, кто с ним не знаком.

5 голосов
/ 07 августа 2013

Я недавно проверил этот поток по той же причине, что и 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');

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

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

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

3 голосов
/ 23 июня 2016

Реальность такова, что вы можете протестировать свой 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 голосов
/ 20 мая 2016

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

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

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

Спасибо!

2 голосов
/ 10 января 2014

Мне всегда нравилась простота концепции 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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...