So che questo thread è piuttosto vecchio a questo punto, ma ho pensato di entrare con i miei pensieri su questo. TL; DR è che a causa della natura non tipizzata e dinamica di JavaScript, puoi effettivamente fare molto senza ricorrere al modello di iniezione di dipendenza (DI) o usando un framework DI. Tuttavia, man mano che un'applicazione diventa più grande e complessa, DI può sicuramente aiutare la manutenibilità del codice.
DI in C #
Per capire perché DI non è un grande bisogno di JavaScript, è utile guardare un linguaggio fortemente tipizzato come C #. (Chiedo scusa a coloro che non conoscono C #, ma dovrebbe essere abbastanza facile da seguire.) Supponiamo che abbiamo un'app che descrive un'auto e il suo clacson. Definiresti due classi:
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();
}
}
Ci sono alcuni problemi con la scrittura del codice in questo modo.
- La
Car
classe è strettamente accoppiata alla particolare implementazione del clacson nella Horn
classe. Se vogliamo cambiare il tipo di clacson utilizzato dall'auto, dobbiamo modificare la Car
classe anche se il suo utilizzo del clacson non cambia. Questo rende anche difficile il test perché non possiamo testare la Car
classe isolatamente dalla sua dipendenza, la Horn
classe.
- La
Car
classe è responsabile del ciclo di vita della Horn
classe. In un semplice esempio come questo non è un grosso problema, ma nelle applicazioni reali le dipendenze avranno dipendenze, che avranno dipendenze, ecc. La Car
classe dovrebbe essere responsabile della creazione dell'intero albero delle sue dipendenze. Questo non è solo complicato e ripetitivo, ma viola la "singola responsabilità" della classe. Dovrebbe concentrarsi sull'essere un'auto, non creare istanze.
- Non è possibile riutilizzare le stesse istanze di dipendenza. Ancora una volta, questo non è importante in questa applicazione giocattolo, ma considera una connessione al database. In genere si avrebbe una singola istanza condivisa nell'applicazione.
Riflettiamo ora questo per usare un modello di iniezione di dipendenza.
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();
}
}
Abbiamo fatto due cose chiave qui. Innanzitutto, abbiamo introdotto un'interfaccia Horn
implementata dalla nostra classe. Questo ci consente di codificare la Car
classe sull'interfaccia anziché la particolare implementazione. Ora il codice potrebbe prendere tutto ciò che implementa IHorn
. In secondo luogo, abbiamo rimosso l'istanza del corno Car
e l'abbiamo passata. Ciò risolve i problemi precedenti e lascia alla funzione principale dell'applicazione la gestione delle istanze specifiche e dei loro cicli di vita.
Ciò significa che potrebbe introdurre un nuovo tipo di clacson da utilizzare senza toccare la Car
classe:
class FrenchHorn : IHorn
{
public void Honk()
{
Console.WriteLine("le beep!");
}
}
FrenchHorn
Invece il main potrebbe semplicemente iniettare un'istanza della classe. Ciò semplifica notevolmente anche i test. È possibile creare una MockHorn
classe da iniettare nel Car
costruttore per assicurarsi di testare solo la Car
classe in isolamento.
L'esempio sopra mostra l'iniezione manuale delle dipendenze. In genere DI viene eseguito con un framework (ad esempio Unity o Ninject nel mondo C #). Questi framework eseguiranno tutto il cablaggio delle dipendenze eseguendo il grafico delle dipendenze e creando le istanze necessarie.
Il modo standard Node.js
Ora diamo un'occhiata allo stesso esempio in Node.js. Probabilmente vorremmo suddividere il nostro codice in 3 moduli:
// 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();
Poiché JavaScript non è tipizzato, non abbiamo lo stesso accoppiamento stretto che avevamo prima. Non sono necessarie interfacce (né esistono) in quanto il car
modulo tenterà semplicemente di chiamare il honk
metodo su qualunque horn
esportazione del modulo.
Inoltre, poiché Node require
memorizza nella cache tutto, i moduli sono essenzialmente singoli memorizzati in un contenitore. Qualsiasi altro modulo che esegue una require
sul horn
modulo di otterrà l'esatto stessa istanza. Ciò rende molto semplice la condivisione di oggetti singleton come connessioni al database.
Ora c'è ancora il problema che il car
modulo è responsabile del recupero della propria dipendenza horn
. Se si desidera che l'auto utilizzi un modulo diverso per il clacson, è necessario modificare la require
dichiarazione nel car
modulo. Questa non è una cosa molto comune da fare, ma causa problemi con i test.
Il solito modo in cui le persone gestiscono il problema del test è con proxyquire . A causa della natura dinamica di JavaScript, proxyquire intercetta le chiamate da richiedere e restituisce invece eventuali matrici / derisioni fornite.
var proxyquire = require('proxyquire');
var hornStub = {
honk: function () {
console.log("test beep!");
}
};
var car = proxyquire('./car', { './horn': hornStub });
// Now make test assertions on car...
Questo è più che sufficiente per la maggior parte delle applicazioni. Se funziona per la tua app, seguila. Tuttavia, nella mia esperienza man mano che le applicazioni diventano più grandi e complesse, mantenere il codice in questo modo diventa più difficile.
DI in JavaScript
Node.js è molto flessibile. Se non sei soddisfatto del metodo sopra, puoi scrivere i tuoi moduli usando il modello di iniezione delle dipendenze. In questo modello, ogni modulo esporta una funzione di fabbrica (o un costruttore di classi).
// 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();
Questo è molto simile al metodo C # in precedenza in quanto il index.js
modulo è responsabile per i cicli di vita e il cablaggio. Il test delle unità è abbastanza semplice in quanto puoi semplicemente passare in beffe / tronconi alle funzioni. Ancora una volta, se questo è abbastanza buono per la tua applicazione, vai con esso.
Bolus DI Framework
A differenza di C #, non esistono framework DI standard standard che possano aiutare nella gestione delle dipendenze. Esistono numerosi framework nel registro npm ma nessuno ha un'adozione diffusa. Molte di queste opzioni sono già state citate nelle altre risposte.
Non ero particolarmente contento di nessuna delle opzioni disponibili, quindi ho scritto il mio bolo chiamato . Bolus è progettato per funzionare con il codice scritto nello stile DI sopra e cerca di essere molto ASCIUTTO e molto semplice. Utilizzando lo stesso identico car.js
e i horn.js
moduli sopra, è possibile riscrivere il index.js
modulo con bolo come:
// index.js
var Injector = require("bolus");
var injector = new Injector();
injector.registerPath("**/*.js");
var car = injector.resolve("car");
car.honkHorn();
L'idea di base è che si crea un iniettore. Registri tutti i tuoi moduli nell'iniettore. Quindi risolvi semplicemente ciò di cui hai bisogno. Bolus seguirà il grafico delle dipendenze e creerà e inietterà dipendenze secondo necessità. Non risparmi molto in un esempio di giocattolo come questo, ma in grandi applicazioni con alberi di dipendenza complicati i risparmi sono enormi.
Bolus supporta una serie di eleganti funzioni come dipendenze opzionali e test globali, ma ci sono due vantaggi chiave che ho visto rispetto all'approccio standard Node.js. Innanzitutto, se hai molte applicazioni simili, puoi creare un modulo npm privato per la tua base che crea un iniettore e registra oggetti utili su di esso. Quindi le tue app specifiche possono aggiungere, sovrascrivere e risolvere secondo necessità, proprio come AngularJSl'iniettore funziona. In secondo luogo, è possibile utilizzare bolo per gestire vari contesti di dipendenze. Ad esempio, è possibile utilizzare il middleware per creare un iniettore figlio per richiesta, registrare l'ID utente, ID sessione, logger, ecc. Sull'iniettore insieme a tutti i moduli a seconda di quelli. Quindi risolvi ciò di cui hai bisogno per soddisfare le richieste. Ciò fornisce istanze dei moduli per richiesta e impedisce di dover passare il logger, ecc. A ogni chiamata di funzione del modulo.