[node.js] Нужна ли мне инъекция зависимостей в NodeJS или как бороться с ...?



Answers

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

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

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

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

Плюсы:

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

Минусы:

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

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

Question

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

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

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




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

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

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

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

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

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




Реальность такова, что вы можете протестировать ваш 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: В контейнерах IoC нет места для JavaScript

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

Миф 3: Инверсия зависимостей === зависимости от инъекций

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

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

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




I discovered this question while answering to an issue on my own DI module asking why one would ever need a DI system for NodeJS programming.

The answer was clearly tending to the ones given in this thread: it depends. There are trade-offs for both approaches and reading this question's answers give a good shape of them.

So, the real answer to this question, should be that in some situations, you would use a DI system, in others not.

That said, what you want as a developer is to not repeat yourself and reuse your services across your various applications.

This means that we should write services that are ready to be used in DI system but not tied to DI libraries. To me, it means that we should write services like this:

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
}

That way your service works not matter if you use it with or without a DI tool.




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

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

var cache = new Cache(mysqlStore);

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

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




Я недавно проверил этот поток по той же причине, что и OP - большинство из libs, с которыми я столкнулся, временно переписывают инструкцию 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');

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

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

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




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

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

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

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 модуль index.js отвечает, например, за жизненные index.js и проводку. Тестирование модулей довольно просто, так как вы можете просто передавать в mocks / stubs функции. Опять же, если это достаточно хорошо для вашего приложения, идите с ним.

Болюс DI Framework

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

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




Node.js requires DI as much as any other platform. If you are building something big, DI will make it easier to mock the dependencies of your code and test your code thoroughly.

Your database layer modules for example, shouldn't just get required at your business code modules because, when unit testing these business code modules, the daos will load and connect to the database.

One solution would be to pass the dependencies as module parameters:

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

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

This way dependencies can be mocked easily and naturally and you can stay focused on testing your code, without using any tricky 3rd party library.

There are other solutions out there (broadway, architect etc) which can help you with this. although they may do more than what you want or use more clutter.




I think other posts have done a great job in the argument for using DI. For me the reasons are

  1. Inject dependencies without knowing their paths. This means that if you change a module location on disk or swap it with another, you don't need to touch every file that depends on it.

  2. It makes it a lot easier to mock dependencies for testing without the pain of overriding the global require function in a way that works without problems.

  3. It helps you organize and reason about you application as loosely coupled modules.

But I had a really hard time finding a DI framework that my team and I can easily adopt. So I recently built a framework called deppie based on these features

  • Minimal API that can be learned in a few minutes
  • No extra code/config/annotations required
  • One to one direct mapping to require modules
  • Can be adopted partially to work with existing code



Google di.js работает над nodejs (+ браузер) (+ ​​ES6)




Related