Я знаю, что эта тема довольно старая на данный момент, но я решила, что подумаю об этом. 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
классе. Если мы хотим изменить тип звукового сигнала, используемого автомобилем, мы должны изменить 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 FrenchHorn : IHorn
{
public void Honk()
{
Console.WriteLine("le beep!");
}
}
FrenchHorn
Вместо этого main может просто внедрить экземпляр класса. Это также значительно упрощает тестирование. Вы можете создать MockHorn
класс для внедрения в Car
конструктор, чтобы убедиться, что вы тестируете только Car
класс в изоляции.
В приведенном выше примере показано внедрение зависимостей вручную. Обычно DI выполняется с помощью фреймворка (например, Unity или Ninject в мире C #). Эти фреймворки будут выполнять всю проводку зависимостей, обходя график зависимостей и создавая экземпляры по мере необходимости.
Стандартный путь Node.js
Теперь давайте посмотрим на тот же пример в Node.js. Мы, вероятно, разбили бы наш код на 3 модуля:
// horn.js
module.exports = {
honk: function () {
console.log("beep!");
}
};
// car.js
var horn = require("./horn");
module.exports = {
honkHorn: function () {
horn.honk();
}
};
// index.js
var car = require("./car");
car.honkHorn();
Поскольку JavaScript нетипизирован, мы не имеем такой же тесной связи, как раньше. Нет необходимости в интерфейсах (и они не существуют), так как car
модуль просто попытается вызвать honk
метод для того, что horn
модуль экспортирует.
Кроме того, поскольку Node require
кэширует все, модули по сути являются синглетонами, которые хранятся в контейнере. Любой другой модуль, который выполняет require
над horn
модулем, получит точно такой же экземпляр. Это делает совместное использование одноэлементных объектов, таких как соединения с базой данных, очень простым.
Теперь все еще существует проблема, заключающаяся в том, что car
модуль отвечает за выбор своей собственной зависимости horn
. Если вы хотите, чтобы автомобиль использовал другой модуль для своего клаксона, вам нужно изменить require
оператор в car
модуле. Это не очень распространенная вещь, но она вызывает проблемы с тестированием.
Обычно люди справляются с проблемой тестирования с помощью proxyquire . Вследствие динамической природы JavaScript, proxyquire перехватывает вызовы require и возвращает любые заглушки / насмешки, которые вы предоставляете.
var proxyquire = require('proxyquire');
var hornStub = {
honk: function () {
console.log("test beep!");
}
};
var car = proxyquire('./car', { './horn': hornStub });
// Now make test assertions on car...
Этого более чем достаточно для большинства приложений. Если это работает для вашего приложения, то иди с ним. Тем не менее, по моему опыту, поскольку приложения становятся все больше и сложнее, поддерживать такой код становится все труднее.
DI в JavaScript
Node.js очень гибкий. Если вы не удовлетворены описанным выше методом, вы можете написать свои модули, используя шаблон внедрения зависимостей. В этом шаблоне каждый модуль экспортирует фабричную функцию (или конструктор класса).
// horn.js
module.exports = function () {
return {
honk: function () {
console.log("beep!");
}
};
};
// car.js
module.exports = function (horn) {
return {
honkHorn: function () {
horn.honk();
}
};
};
// index.js
var horn = require("./horn")();
var car = require("./car")(horn);
car.honkHorn();
Это очень похоже на метод C # ранее в том, что index.js
модуль отвечает за жизненные циклы экземпляров и проводку. Модульное тестирование довольно простое, так как вы можете просто передать макеты / заглушки функциям. Опять же, если это достаточно хорошо для вашего приложения, используйте его.
Bolus DI Framework
В отличие от C #, не существует установленных стандартных структур DI, которые бы помогли вам в управлении зависимостями. В реестре npm есть несколько платформ, но ни одна из них не получила широкого распространения. Многие из этих вариантов уже упоминались в других ответах.
Я не был особенно доволен ни одним из доступных вариантов, поэтому я написал свой болюс . Bolus предназначен для работы с кодом, написанным в стиле DI выше, и старается быть очень СУХИМЫМ и очень простым. Используя тот же самый car.js
и horn.js
модулей выше, вы можете переписать index.js
модуль с болюса , как:
// index.js
var Injector = require("bolus");
var injector = new Injector();
injector.registerPath("**/*.js");
var car = injector.resolve("car");
car.honkHorn();
Основная идея заключается в том, что вы создаете инжектор. Вы регистрируете все свои модули в инжекторе. Тогда вы просто решаете, что вам нужно. Bolus будет обходить граф зависимостей и создавать и вводить зависимости по мере необходимости. В таком игрушечном примере вы не сильно экономите, но в больших приложениях со сложными деревьями зависимостей экономия огромна.
Bolus поддерживает множество полезных функций, таких как необязательные зависимости и тестовые глобалы, но я вижу два ключевых преимущества по сравнению со стандартным подходом Node.js. Во-первых, если у вас много похожих приложений, вы можете создать частный модуль npm для своей базы, который создает инжектор и регистрирует на нем полезные объекты. Тогда ваши конкретные приложения могут добавлять, переопределять и разрешать по мере необходимости так же, как AngularJSинжектор работает. Во-вторых, вы можете использовать болюс для управления различными контекстами зависимостей. Например, вы можете использовать промежуточное ПО для создания дочернего инжектора для каждого запроса, регистрации идентификатора пользователя, идентификатора сеанса, регистратора и т. Д. В инжекторе вместе с любыми модулями, в зависимости от них. Затем решите, что вам нужно для обслуживания запросов. Это дает вам экземпляры ваших модулей для каждого запроса и предотвращает необходимость передавать регистратор и т. Д. При каждом вызове функции модуля.