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