Я знаю, что эта тема довольно старая на данный момент, но я решила, что подумаю об этом.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();
}
}
При написании кода существует несколько проблем.
- Класс
Car
тесно связан с конкретной реализацией hornв классе Horn
.Если мы хотим изменить тип звукового сигнала, используемого автомобилем, мы должны изменить класс Car
, даже если использование звукового сигнала не изменится.Это также затрудняет тестирование, поскольку мы не можем тестировать класс Car
в отрыве от его зависимости, класс Horn
. - Класс
Car
отвечает за жизненный цикл класса Horn
,В простом примере, подобном этому, это не является большой проблемой, но в реальных приложениях зависимости будут иметь зависимости, которые будут иметь зависимости и т. Д. Класс Car
должен отвечать за создание всего дерева своих зависимостей.Это не только сложно и повторяется, но и нарушает «единственную ответственность» класса.Следует сосредоточиться на том, чтобы быть автомобилем, а не создавать экземпляры. - Невозможно повторно использовать одни и те же экземпляры зависимости.Опять же, это не важно в этом игрушечном приложении, но рассмотрим соединение с базой данных.Обычно у вас есть один экземпляр, который используется в вашем приложении.
Теперь давайте проведем рефакторинг для использования шаблона внедрения зависимостей.
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 инжектор. Во-вторых, вы можете использовать болюс для управления различными контекстами зависимостей. Например, вы можете использовать промежуточное ПО для создания дочернего инжектора для каждого запроса, регистрации идентификатора пользователя, идентификатора сеанса, регистратора и т. Д. В инжекторе вместе с любыми модулями, в зависимости от них. Затем решите, что вам нужно для обслуживания запросов. Это дает вам экземпляры ваших модулей для каждого запроса и предотвращает необходимость передавать регистратор и т. Д. При каждом вызове функции модуля.