Quando não é apropriado usar o padrão de injeção de dependência?

112

Desde que aprendi (e amo) os testes automatizados, encontrei-me usando o padrão de injeção de dependência em quase todos os projetos. É sempre apropriado usar esse padrão ao trabalhar com testes automatizados? Existem situações em que você deveria evitar o uso de injeção de dependência?

    
por Tom Squires 20.02.2012 / 18:57
fonte

7 respostas

111

Basicamente, a injeção de dependência faz algumas suposições (geralmente, mas nem sempre válidas) sobre a natureza de seus objetos. Se isso estiver errado, a DI pode não ser a melhor solução:

  • Primeiro, basicamente, a DI assume que o acoplamento rígido de implementações de objetos é SEMPRE ruim . Esta é a essência do Princípio da Inversão da Dependência: "uma dependência nunca deve ser feita após uma concreção; somente sobre uma abstração".

    Isso fecha o objeto dependente para alterar com base em uma alteração na implementação concreta; uma classe dependendo do ConsoleWriter especificamente precisará mudar se a saída precisar ir para um arquivo, mas se a classe for dependente apenas de um IWriter expondo um método Write (), podemos substituir o ConsoleWriter atualmente sendo usado por um FileWriter e classe dependente não saberia a diferença (Princípio de Substituição de Liskhov).

    No entanto, um design NUNCA pode ser fechado para todos os tipos de alteração; se o design da interface do IWriter for alterado, para adicionar um parâmetro ao Write (), um objeto de código extra (a interface do IWriter) deverá ser alterado, na parte superior do objeto / método de implementação e seu (s) uso (s). Se as mudanças na interface atual forem mais prováveis do que as mudanças na implementação da referida interface, o acoplamento frouxo (e o DI-ing de dependências fracamente acopladas) pode causar mais problemas do que resolve.

  • Segundo, e o corolário, DI assume que a classe dependente NUNCA é um bom lugar para criar uma dependência . Isso vai para o Princípio da Responsabilidade Única; se você tiver um código que crie uma dependência e também a use, haverá dois motivos pelos quais a classe dependente pode ter que mudar (uma alteração no uso OU na implementação), violando o SRP.

    No entanto, novamente, adicionar camadas de indireção para DI pode ser uma solução para um problema que não existe; Se é lógico encapsular a lógica em uma dependência, mas essa lógica é a única implementação de uma dependência, então é mais doloroso codificar a resolução da dependência (injeção, local de serviço, fábrica) do que seria apenas use new e esqueça.

  • Por fim, a DI, por sua natureza, centraliza o conhecimento de todas as dependências e suas implementações . Isso aumenta o número de referências que a montagem que executa a injeção deve ter e, na maioria dos casos, NÃO reduz o número de referências requeridas pelos conjuntos de classes dependentes reais.

    ALGO, EM ALGUM LUGAR, deve ter conhecimento do dependente, da interface de dependência e da implementação de dependência para "conectar os pontos" e satisfazer essa dependência. DI tende a colocar todo esse conhecimento em um nível muito alto, seja em um contêiner IoC ou no código que cria objetos "principais", como o formulário principal ou o Controlador, que deve hidratar (ou fornecer métodos de fábrica para) as dependências. Isso pode colocar um monte de código necessariamente bem acoplado e muitas referências de montagem em altos níveis de seu aplicativo, que só precisa desse conhecimento para "escondê-lo" das classes dependentes reais (que de uma perspectiva muito básica é a melhor lugar para ter esse conhecimento, onde é usado).

    Normalmente também não remove as referências mencionadas de baixo para baixo no código; um dependente ainda deve referenciar a biblioteca que contém a interface para sua dependência, que está em um dos três locais:

    • tudo em uma única montagem de "Interfaces" que se torna muito centrada em aplicativos,
    • cada um junto com a (s) implementação (ões) principal (is), removendo a vantagem de não ter que recompilar os dependentes quando as dependências mudam, ou
    • um ou dois cada em montagens altamente coesas, o que aumenta a contagem de montagens, aumenta drasticamente os tempos de "construção total" e diminui o desempenho do aplicativo.

    Tudo isso, mais uma vez, para resolver um problema em lugares onde não haja nenhum.

por 21.02.2012 / 20:11
fonte
27
Fora das estruturas de injeção de dependência, a injeção de dependência (via injeção de construtor ou injetor setter) é quase um jogo de soma zero: você diminui o acoplamento entre o objeto A e sua dependência B, mas agora qualquer objeto que precise de uma instância de Um deve agora também construir o objeto B.

Você reduziu ligeiramente o acoplamento entre A e B, mas reduziu o encapsulamento de A e aumentou o acoplamento entre A e qualquer classe que deve construir uma instância de A, acoplando-as também às dependências de A.

Portanto, a injeção de dependência (sem um framework) é tão prejudicial quanto útil.

O custo extra é muitas vezes facilmente justificável, no entanto: se o código do cliente sabe mais sobre como construir a dependência do que o próprio objeto, então a injeção de dependência realmente reduz o acoplamento; por exemplo, um Scanner não sabe muito sobre como obter ou construir um fluxo de entrada para analisar a entrada, ou de qual fonte o código do cliente deseja analisar a entrada, portanto, a injeção do construtor de um fluxo de entrada é a solução óbvia. p>

O teste é outra justificativa, para poder usar dependências simuladas. Isso deve significar adicionar um construtor extra usado apenas para testes que permite que as dependências sejam injetadas: se você mudar seus construtores para sempre requerer que as dependências sejam injetadas, de repente, você precisa saber sobre as dependências de dependências de suas dependências para construir suas dependências. dependências diretas, e você não pode fazer nenhum trabalho.

Pode ser útil, mas você deve definitivamente se perguntar por cada dependência, se o benefício do teste vale o custo, e eu realmente vou querer zombar dessa dependência durante o teste?

Quando uma estrutura de injeção de dependência é adicionada e a construção de dependências é delegada não ao código do cliente, mas sim à estrutura, a análise de custo / benefício muda bastante.

Em uma estrutura de injeção de dependência, as compensações são um pouco diferentes; o que você está perdendo ao injetar uma dependência é a capacidade de saber com facilidade em qual implementação você está confiando e mudando a responsabilidade de decidir em que dependência você está confiando para algum processo de resolução automatizado (por exemplo, se precisarmos de um @ Inject'ed Foo , deve haver algo que o @Provides Foo, e cujas dependências injetadas estejam disponíveis), ou algum arquivo de configuração de alto nível que prescreva qual provedor deve ser usado para cada recurso, ou algum híbrido dos dois (por exemplo, ser um processo de resolução automatizado para dependências que podem ser substituídas, se necessário, usando um arquivo de configuração).

Como na injeção de construtor, acho que a vantagem de fazer isso acaba sendo, de novo, muito semelhante ao custo de fazer isso: você não precisa saber quem está fornecendo os dados de que depende e, se houver são múltiplos provedores em potencial, você não precisa conhecer a ordem preferida para verificar fornecedores, certifique-se de que todos os locais que precisam de dados verifiquem todos os provedores em potencial, etc., porque tudo isso é tratado em alto nível por a plataforma de injeção de dependência.

Embora eu pessoalmente não tenha muita experiência com estruturas de DI, a minha impressão é que eles fornecem mais benefícios do que custos quando a dor de cabeça de encontrar o fornecedor correto dos dados ou serviços que você precisa tem um custo maior do que a dor de cabeça, quando algo falha, de não saber imediatamente localmente qual código forneceu os dados ruins que causaram uma falha posterior em seu código.

Em alguns casos, outros padrões que obscurecem dependências (por exemplo, localizadores de serviço) já haviam sido adotados (e talvez também comprovado seu valor) quando estruturas de DI apareceram em cena, e as estruturas de DI foram adotadas porque ofereciam alguma vantagem competitiva, como, por exemplo, exigir menos código clichê ou, potencialmente, fazer menos para obscurecer o provedor de dependência quando for necessário determinar qual provedor está realmente em uso.

    
por 21.02.2012 / 01:39
fonte
11
  1. se você estiver criando entidades de banco de dados, você deve preferir ter alguma classe de fábrica que você irá injetar no seu controlador,

  2. se você precisar criar objetos primitivos como ints ou longs. Além disso, você deve criar "à mão" a maioria dos objetos de biblioteca padrão, como datas, guias, etc.

  3. se você quiser injetar strings de configuração, provavelmente é melhor injetar alguns objetos de configuração (em geral, recomenda-se envolver tipos simples em objetos significativos: int temperatureInCelsiusDegrees - > CelciusDeegree temperature)

E não use o Service locator como alternativa de injeção de dependência, é um anti-padrão, mais informações: link

    
por 20.02.2012 / 23:09
fonte
8

Quando você não quiser ganhar nada, tornando seu projeto sustentável e testável.

Sério, eu amo IoC e DI em geral, e eu diria que 98% das vezes eu vou usar esse padrão sem falhar. É especialmente importante em um ambiente multiusuário, onde o código pode ser reutilizado novamente por diferentes membros da equipe e projetos diferentes, já que separa a lógica da implementação. Logging é um excelente exemplo disso, uma interface ILog injetada em uma classe é mil vezes mais fácil de manter do que simplesmente conectar sua estrutura de logging-du-jour, já que você não tem garantia de que outro projeto usará a mesma estrutura de logging um em tudo!).

No entanto, há momentos em que não é um padrão aplicável. Por exemplo, pontos de entrada funcionais que são implementados em um contexto estático por um inicializador não substituível (WebMethods, estou olhando para você, mas seu método Main () em sua classe Program é outro exemplo) simplesmente não pode ter dependências injetadas na inicialização Tempo. Eu também diria que um protótipo, ou qualquer código investigativo descartável, também é um mau candidato; os benefícios do DI são benefícios de médio a longo prazo (testabilidade e facilidade de manutenção), se você tiver certeza de que descartará a maior parte de um código dentro de uma semana, eu diria que você não ganha nada isolando dependências, basta gastar o tempo que você normalmente passaria testando e isolando as dependências fazendo o código funcionar.

Em suma, é sensato adotar uma abordagem pragmática em qualquer metodologia ou padrão, pois nada é aplicável 100% do tempo.

Uma coisa a notar é seu comentário sobre testes automatizados: minha definição disso é testes funcionais automatizados, por exemplo testes de selênio com script, se você estiver em um contexto da web. Estes são geralmente testes completamente black-box, sem necessidade de saber sobre o funcionamento interno do código. Se você estivesse se referindo a Testes de Unidade ou Integração, eu diria que o padrão de DI é quase sempre aplicável a qualquer projeto que depende muito desse tipo de teste de caixa branca, já que, por exemplo, ele permite testar coisas como métodos que tocam o DB sem qualquer necessidade de um DB estar presente.

    
por 21.02.2012 / 20:42
fonte
1

Esta não é uma resposta completa, mas apenas outro ponto.

Quando você tem um aplicativo que inicia uma vez, é executado por um longo tempo (como um aplicativo da web), a DI pode ser boa.

Quando você tem um aplicativo iniciado várias vezes e executado por períodos mais curtos (como um aplicativo para dispositivos móveis), provavelmente não deseja o contêiner.

    
por 16.09.2016 / 09:28
fonte
0

Enquanto as outras respostas se concentram nos aspectos técnicos, gostaria de acrescentar uma dimensão prática.

Ao longo dos anos, cheguei à conclusão de que há vários requisitos práticos que precisam ser atendidos se a introdução da injeção de dependência for um sucesso.

  1. Deve haver um motivo para introduzi-lo.

    Isto parece óbvio, mas se o seu código apenas obtém as coisas da base de dados e as devolve sem qualquer lógica, então adicionar um contêiner DI torna as coisas mais complexas, com benefícios não reais. O teste de integração seria mais importante aqui.

  2. A equipe precisa ser treinada e integrada.

    A menos que a maioria da equipe esteja a bordo e entenda que a adição de inversão de contêiner de controle torna-se uma outra maneira de fazer as coisas e torna a base de código ainda mais complicada.

    Se a DI é introduzida por um novo membro da equipe, porque eles entendem e gostam e querem apenas mostrar que são bons, E a equipe não está ativamente envolvida, existe um risco real de que ela realmente diminuir a qualidade do código.

  3. Você precisa testar

    Embora o desacoplamento seja geralmente uma coisa boa, o DI pode mover a resolução de uma dependência do tempo de compilação para o tempo de execução. Isso é realmente muito perigoso se você não testar bem. Falhas na resolução do tempo de execução podem ser caras para rastrear e resolver.
    (É claro pelo seu teste que você faz, mas muitas equipes não testam na medida em que é exigido pelo DI.)

por 16.09.2016 / 02:27
fonte
-1

Uma alternativa à Injeção de dependência está usando um Service Locator . Um Service Locator é mais fácil de entender, depurar e torna a construção de um objeto mais simples, especialmente se você não estiver usando uma estrutura de DI. Os Service Locators são um bom padrão para gerenciar dependências estáticas externas, por exemplo, um banco de dados que você teria de passar para todos os objetos na camada de acesso a dados.

Ao refatorar o código legado , muitas vezes é mais fácil refatorar para um Service Locator do que para Dependency Injection. Tudo o que você faz é substituir instanciações com pesquisas de serviço e depois falsificar o serviço em seu teste de unidade.

No entanto, existem algumas desvantagens no Service Locator. Conhecer as depandâncias de uma classe é mais difícil porque as dependências estão ocultas na implementação da classe, não em construtores ou setters. E criar dois objetos que dependem de diferentes implementações do mesmo serviço é difícil ou impossível.

TLDR : se a sua classe tiver dependências estáticas ou se você estiver refatorando o código legado, um Service Locator é possivelmente melhor que DI.

    
por 20.02.2012 / 19:54
fonte