Eu sei que esse tópico é bastante antigo neste momento, mas achei que iria concordar com meus pensamentos sobre isso. O TL; DR é que, devido à natureza dinâmica e sem tipo do JavaScript, você pode realmente fazer bastante sem recorrer ao padrão de injeção de dependência (DI) ou usar uma estrutura de DI. No entanto, à medida que um aplicativo se torna maior e mais complexo, o DI pode definitivamente ajudar na manutenção do seu código.
DI em C #
Para entender por que o DI não é tão necessário em JavaScript, é útil olhar para uma linguagem fortemente tipada como C #. (Desculpas para quem não conhece C #, mas deve ser fácil o suficiente.) Diga que temos um aplicativo que descreve um carro e sua buzina. Você definiria duas classes:
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();
}
}
Existem alguns problemas ao escrever o código dessa maneira.
- A
Car
classe está fortemente acoplada à implementação específica da buzina na Horn
classe. Se queremos mudar o tipo de buzina usada pelo carro, temos que modificar a Car
classe, mesmo que o uso da buzina não mude. Isso também dificulta o teste porque não podemos testar a Car
classe isoladamente de sua dependência, a Horn
classe.
- A
Car
classe é responsável pelo ciclo de vida da Horn
classe. Em um exemplo simples como esse, não é um grande problema, mas em aplicativos reais as dependências terão dependências, que terão dependências etc. A Car
classe precisaria ser responsável por criar a árvore inteira de suas dependências. Isso não é apenas complicado e repetitivo, mas viola a "responsabilidade única" da classe. Deve se concentrar em ser um carro, não em criar instâncias.
- Não há como reutilizar as mesmas instâncias de dependência. Novamente, isso não é importante neste aplicativo de brinquedo, mas considere uma conexão com o banco de dados. Você normalmente teria uma única instância que é compartilhada em seu aplicativo.
Agora, vamos refatorar isso para usar um padrão de injeção de dependência.
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();
}
}
Fizemos duas coisas importantes aqui. Primeiro, introduzimos uma interface que nossa Horn
classe implementa. Isso nos permite codificar a Car
classe para a interface, em vez da implementação específica. Agora o código pode levar qualquer coisa que seja implementada IHorn
. Segundo, retiramos a instanciação da buzina Car
e a transmitimos. Isso resolve os problemas acima e deixa a função principal do aplicativo gerenciar instâncias específicas e seus ciclos de vida.
O que isto significa é que poderia introduzir um novo tipo de buzina para o carro usar sem tocar na Car
classe:
class FrenchHorn : IHorn
{
public void Honk()
{
Console.WriteLine("le beep!");
}
}
O main pode apenas injetar uma instância da FrenchHorn
classe. Isso também simplifica drasticamente os testes. Você pode criar uma MockHorn
classe para injetar no Car
construtor para garantir que você está testando apenas a Car
classe isoladamente.
O exemplo acima mostra injeção de dependência manual. Normalmente, o DI é feito com uma estrutura (por exemplo, Unity ou Ninject no mundo C #). Essas estruturas farão toda a fiação de dependência para você, percorrendo seu gráfico de dependência e criando instâncias, conforme necessário.
A maneira padrão do Node.js
Agora vamos ver o mesmo exemplo no Node.js. Provavelmente dividiríamos nosso código em 3 módulos:
// 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();
Como o JavaScript não é digitado, não temos o mesmo acoplamento rígido que tínhamos antes. Não há necessidade de interfaces (nem elas existem), pois o car
módulo apenas tentará chamar o honk
método em qualquer que seja a horn
exportação do módulo.
Além disso, como o Node require
armazena em cache tudo, os módulos são essencialmente singletons armazenados em um contêiner. Qualquer outro módulo que execute um require
no horn
módulo obterá exatamente a mesma instância. Isso facilita muito o compartilhamento de objetos singleton, como conexões com o banco de dados.
Agora ainda há o problema de o car
módulo ser responsável por buscar sua própria dependência horn
. Se você quisesse que o carro usasse um módulo diferente para a buzina, seria necessário alterar a require
declaração no car
módulo. Isso não é algo muito comum, mas causa problemas com o teste.
A maneira usual pelas quais as pessoas lidam com o problema de teste é com o proxyquire . Devido à natureza dinâmica do JavaScript, o proxyquire intercepta chamadas para exigir e retorna todos os stubs / zombarias que você fornece.
var proxyquire = require('proxyquire');
var hornStub = {
honk: function () {
console.log("test beep!");
}
};
var car = proxyquire('./car', { './horn': hornStub });
// Now make test assertions on car...
Isso é mais do que suficiente para a maioria dos aplicativos. Se funcionar para o seu aplicativo, vá com ele. No entanto, na minha experiência à medida que os aplicativos se tornam maiores e mais complexos, manter um código como esse se torna mais difícil.
DI em JavaScript
O Node.js é muito flexível. Se você não estiver satisfeito com o método acima, poderá escrever seus módulos usando o padrão de injeção de dependência. Nesse padrão, todo módulo exporta uma função de fábrica (ou um construtor de classe).
// 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();
Isso é muito análogo ao método C # anteriormente, pois o index.js
módulo é responsável por ciclos de vida e fiação. O teste de unidade é bastante simples, pois você pode simplesmente passar zombarias / stubs para as funções. Novamente, se isso for bom o suficiente para o seu aplicativo, vá com ele.
Bolus DI Framework
Ao contrário do C #, não existem estruturas de DI padrão estabelecidas para ajudar no gerenciamento de dependências. Há várias estruturas no registro npm, mas nenhuma possui adoção generalizada. Muitas dessas opções já foram citadas nas outras respostas.
Eu não estava particularmente feliz com nenhuma das opções disponíveis, então escrevi meu próprio bolus . O Bolus foi projetado para funcionar com o código escrito no estilo DI acima e tenta ser muito SECO e muito simples. Usando exatamente o mesmo car.js
e os horn.js
módulos acima, você pode reescrever o index.js
módulo com bolus como:
// index.js
var Injector = require("bolus");
var injector = new Injector();
injector.registerPath("**/*.js");
var car = injector.resolve("car");
car.honkHorn();
A idéia básica é que você crie um injetor. Você registra todos os seus módulos no injetor. Então você simplesmente resolve o que precisa. O Bolus percorrerá o gráfico de dependência e criará e injetará dependências, conforme necessário. Você não economiza muito em um exemplo de brinquedo como esse, mas em aplicativos grandes com árvores de dependência complicadas, as economias são enormes.
O Bolus suporta vários recursos interessantes, como dependências opcionais e globais de teste, mas há dois benefícios principais que eu já vi em relação à abordagem padrão do Node.js. Primeiro, se você tiver muitos aplicativos semelhantes, poderá criar um módulo npm privado para sua base que crie um injetor e registre objetos úteis nele. Em seguida, seus aplicativos específicos podem adicionar, substituir e resolver, conforme necessário, da mesma forma que o AngularJSinjetor funciona. Segundo, você pode usar o bolus para gerenciar vários contextos de dependências. Por exemplo, você pode usar o middleware para criar um injetor secundário por solicitação, registrar o ID do usuário, o ID da sessão, o registrador etc. no injetor, juntamente com quaisquer módulos, dependendo deles. Em seguida, resolva o que você precisa para atender solicitações. Isso fornece instâncias de seus módulos por solicitação e evita a necessidade de passar o criador de logs etc. para todas as chamadas de função do módulo.