Muitos desenvolvedores do Node.js. usam dependências físicas do módulo (exclusivamente) usando o require () para ligar módulos, mas há outras abordagens com seus prós e contras. Vou falar sobre eles neste artigo. Quatro abordagens serão consideradas:
- Dependências rígidas (require ())
- Injeção de Dependência
- Localizador de Serviços
- Contêineres de dependência incorporados (contêiner DI)
Um pouco sobre módulos
Módulos e arquitetura modular são a base do Node.js. Os módulos fornecem encapsulamento (ocultando detalhes de implementação e abrindo apenas a interface usando module.exports), reutilização de código, divisão lógica em arquivos. Quase todos os aplicativos Node.js consistem em muitos módulos que precisam interagir de alguma forma. Se você vincular incorretamente os módulos ou até deixar a interação dos módulos desviar, poderá descobrir rapidamente que o aplicativo começa a "desmoronar": alterações no código em um local levam a uma quebra em outro e o teste de unidade se torna simplesmente impossível. Idealmente, os módulos devem ter alta
conectividade , mas baixo
acoplamento .
Vícios difíceis
Uma forte dependência de um módulo em outro ocorre quando require () é usado. Essa é uma abordagem eficaz, simples e comum. Por exemplo, queremos apenas conectar o módulo responsável pela interação com o banco de dados:
Prós:
- Simplicidade
- Organização visual dos módulos
- Depuração fácil
Contras:
- Dificuldade para reutilizar o módulo (por exemplo, se quisermos usar nosso módulo repetidamente, mas com uma instância diferente do banco de dados)
- Dificuldade para teste de unidade (você precisa criar uma instância de banco de dados fictícia e, de alguma forma, passá-la ao módulo)
Resumo:
A abordagem é boa para pequenas aplicações ou protótipos, bem como para conectar módulos sem estado: fábricas, designers e conjuntos de recursos.
Injeção de Dependência
A idéia principal da injeção de dependência é transferir dependências de um componente externo para o módulo. Assim, a forte dependência no módulo é eliminada e torna-se possível reutilizá-lo em diferentes contextos (por exemplo, com diferentes instâncias de banco de dados).
A injeção de dependência pode ser implementada passando a dependência no argumento do construtor ou definindo as propriedades do módulo, mas na prática é melhor usar o primeiro método. Vamos aplicar a implementação de dependências na prática, criando uma instância do banco de dados usando a fábrica e passando-a para o nosso módulo:
Módulo externo:
const dbFactory = require('db'); const OurModule = require('./ourModule.js'); const dbInstance = dbFactory.createInstance('instance1'); const ourModule = OurModule(dbInstance);
Agora, não podemos apenas reutilizar nosso módulo, mas também escrever facilmente um teste de unidade para ele: basta criar um objeto simulado para a instância do banco de dados e passá-lo ao módulo.
Prós:
- Facilidade de escrever testes de unidade
- Aumentar a reutilização dos módulos
- Menor envolvimento, maior conectividade
- Mudar a responsabilidade pela criação de dependências para um nível mais alto - geralmente isso melhora a legibilidade do programa, uma vez que dependências importantes são coletadas em um único local e não distribuídas por módulos
Contras:
- A necessidade de um design de dependência mais completo: por exemplo, uma certa ordem de inicialização do módulo deve ser seguida
- A complexidade do gerenciamento de dependências, especialmente quando há muitos
- Deterioração da compreensibilidade do código do módulo: escrever o código do módulo quando uma dependência vem de fora é mais difícil, porque não podemos olhar diretamente para essa dependência.
Resumo:
A injeção de dependência aumenta a complexidade e o tamanho do aplicativo, mas, em troca, permite a reutilização e facilita o teste. O desenvolvedor deve decidir o que é mais importante para ele em um caso específico - a simplicidade de uma dependência difícil ou as possibilidades mais amplas de introdução de uma dependência.
Localizador de Serviços
A idéia é ter um registro de dependência que atue como intermediário ao carregar uma dependência com qualquer módulo. Em vez de ligação direta, as dependências são solicitadas pelo módulo ao localizador de serviço. Obviamente, os módulos têm uma nova dependência - o próprio localizador de serviço. Um exemplo de localizador de serviço é o sistema do módulo Node.js.: os módulos solicitam uma dependência usando require (). No exemplo a seguir, criaremos um localizador de serviço, registramos instâncias de banco de dados e nosso módulo nele.
Módulo externo:
const serviceLocator = require('./serviceLocator.js')(); serviceLocator.register('someParameter', 'someValue'); serviceLocator.factory('db', require('db')); serviceLocator.factory('ourModule', require('ourModule')); const ourModule = serviceLocator.get('ourModule');
Nosso módulo:
Deve-se notar que o localizador de serviço armazena fábricas de serviço em vez de instâncias, e isso faz sentido. Temos os benefícios da inicialização lenta e agora não precisamos nos preocupar com a ordem de inicialização dos módulos - todos os módulos serão inicializados quando necessário. Além disso, tivemos a oportunidade de armazenar parâmetros no localizador de serviço (consulte "someParameter").
Prós:
- Facilidade de escrever testes de unidade
- Reutilizar um módulo é mais fácil do que com um vício intenso
- Engajamento reduzido, maior conectividade em comparação com o vício intenso
- Mudando a responsabilidade de criar dependências para um nível superior
- Não é necessário seguir a ordem de inicialização do módulo
Contras:
- Reutilizar um módulo é mais difícil do que implementar uma dependência (devido à dependência adicional do localizador de serviço)
- Legibilidade: é ainda mais difícil entender o que a dependência exigida pelo localizador de serviço faz
- Maior envolvimento comparado à injeção de dependência
Sumário
Em geral, um localizador de serviço é semelhante à injeção de dependência; de certa forma, é mais fácil (não há ordem de inicialização); em alguns casos, é mais difícil (menor do que a possibilidade de reutilizar código).
Contêineres de dependência incorporados (contêiner DI)
O localizador de serviço tem uma desvantagem, devido à qual raramente é aplicado na prática - a dependência dos módulos no próprio localizador. Contêineres de dependência incorporados (contêineres DI) não têm essa desvantagem. De fato, este é o mesmo localizador de serviço com uma função adicional que determina as dependências do módulo antes de criar sua instância. Você pode determinar as dependências do módulo analisando e extraindo argumentos do construtor do módulo (em JavaScript, é possível converter um link para uma função em uma string usando toString ()). Este método é adequado se o desenvolvimento for puramente para o servidor. Se o código do cliente é gravado, ele é freqüentemente minificado e não faz sentido extrair os nomes dos argumentos. Nesse caso, a lista de dependências pode ser passada como uma matriz de seqüências de caracteres (no Angular.js, com base no uso de contêineres DI, essa abordagem é usada). Implementamos o contêiner de DI usando a análise de argumentos do construtor:
const fnArgs = require('parse-fn-args'); module.exports = function() { const dependencies = {}; const factories = {}; const diContainer = {}; diContainer.factory = (name, factory) => { factories[name] = factory; }; diContainer.register = (name, dep) => { dependencies[name] = dep; }; diContainer.get = (name) => { if(!dependencies[name]) { const factory = factories[name]; dependencies[name] = factory && diContainer.inject(factory); if(!dependencies[name]) { throw new Error('Cannot find module: ' + name); } } diContainer.inject = (factory) => { const args = fnArgs(factory) .map(dependency => diContainer.get(dependency)); return factory.apply(null, args); } return dependencies[name]; };
Comparado ao localizador de serviço, o método injetar foi adicionado, o que determina as dependências do módulo antes de criar sua instância. O código do módulo externo não mudou muito:
const diContainer = require('./diContainer.js')(); diContainer.register('someParameter', 'someValue'); diContainer.factory('db', require('db')); diContainer.factory('ourModule', require('ourModule')); const ourModule = diContainer.get('ourModule');
Nosso módulo tem exatamente a mesma aparência de uma injeção simples de dependência:
Agora, nosso módulo pode ser chamado com a ajuda de um contêiner de DI e transmitindo diretamente as instâncias de dependência necessárias, usando uma injeção simples de dependência.
Prós:
- Facilidade de escrever testes de unidade
- Fácil reutilização de módulos
- Menor envolvimento, maior conectividade dos módulos (especialmente em comparação com um localizador de serviço)
- Mudando a responsabilidade de criar dependências para um nível superior
- Não há necessidade de acompanhar a inicialização do módulo
O maior menos:
- Complicação significativa da lógica de ligação do módulo
Sumário
Essa abordagem é mais difícil de entender e contém um pouco mais de código, mas vale a pena o tempo gasto por causa de seu poder e elegância. Em projetos pequenos, essa abordagem pode ser redundante, mas deve ser considerada se um aplicativo grande estiver sendo projetado.
Conclusão
As abordagens básicas para a ligação de módulos no Node.js. foram consideradas. Como geralmente acontece, a “bala de prata” não existe, mas o desenvolvedor deve estar ciente das alternativas possíveis e escolher a solução mais adequada para cada caso específico.
O artigo é baseado em um capítulo do livro
Node.js. Design Patterns , lançado em 2017. Infelizmente, muitas coisas no livro já estão desatualizadas, então não recomendo 100% a leitura, mas algumas coisas ainda são relevantes hoje.