Я знаю, що ця нитка є досить старою на даний момент, але я подумав, що я можу зазвучити свої думки з цього приводу. 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
класу. Це також значно спрощує тестування. Ви можете створити MockHorn
клас, який потрібно вводити в Car
конструктор, щоб переконатися, що ви просто Car
ізолюєте клас.
Наведений вище приклад показує ручне введення залежності. Зазвичай DI робиться з рамкою (наприклад, Єдність або Нінжект у світі 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
модулі. Це не дуже поширена річ, але це викликає проблеми з тестуванням.
Звичайний спосіб вирішення проблеми тестування - це проксі-запит . Завдяки динамічній природі JavaScript проксі-сервер перехоплює дзвінки, щоб вимагати та повертати будь-які створені вами заглушки / макети.
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
модуль відповідає за життєві цикли та проводку. Тестування блоків досить просте, оскільки ви можете просто передати макети / заглушки до функцій. Знову ж таки, якщо це досить добре для вашої заявки, займіться цим.
Рамка Болюс Д.І.
На відміну від 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 підтримує купу чудових функцій, таких як необов'язкові залежності та тестові глобали, але є два ключові переваги, які я бачив щодо стандартного підходу Node.js. По-перше, якщо у вас багато подібних додатків, ви можете створити приватний модуль npm для своєї бази, який створює інжектор і реєструє на ньому корисні об’єкти. Тоді ваші конкретні програми можуть додавати, змінювати та вирішувати за потребою так само, як і AngularJSпрацює інжектор. По-друге, ви можете використовувати болюс для управління різними контекстами залежностей. Наприклад, ви можете використовувати проміжне програмне забезпечення для створення дочірнього інжектора за запитом, реєстрації ідентифікатора користувача, ідентифікатора сесії, реєстратора тощо на інжекторі разом із будь-якими модулями, залежно від них. Потім вирішіть, що потрібно для обслуговування запитів. Це дає вам екземпляри ваших модулів за запитом і запобігає необхідності передавати реєстратор тощо під час кожного виклику функції модуля.