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

124

Desde que aprendi (e amei) os testes automatizados, me vi 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ê deve evitar o uso de injeção de dependência?

Tom Squires
fonte
1
Relacionado (provavelmente não duplicado) - stackoverflow.com/questions/2407540/…
Steve314
3
Soa um pouco como o anti-padrão do Golden Hammer. en.wikipedia.org/wiki/Anti-pattern
Cuga 01/10/13

Respostas:

123

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

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

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

    No entanto, um design NUNCA pode ser fechado para todos os tipos de alterações; se o design da interface IWriter for alterado, para adicionar um parâmetro a Write (), um objeto de código extra (a interface IWriter) deverá ser alterado agora, sobre o objeto / método de implementação e seus usos. Se as mudanças na interface real são mais prováveis ​​do que as mudanças na implementação da referida interface, o acoplamento frouxo (e a separação de dependências fracamente acopladas) pode causar mais problemas do que resolve.

  • Segundo, e corolário, DI assume que a classe dependente NUNCA é um bom lugar para criar uma dependência . Isso vai para o princípio de responsabilidade única; se você possui um código que cria uma dependência e também a usa, há duas razões pelas quais a classe dependente pode precisar 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 dessa dependência, é mais doloroso codificar a resolução fracamente acoplada da dependência (injeção, local de serviço, fábrica) do que seria apenas usar newe esquecer.

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

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

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

    • tudo em um único conjunto "Interfaces" que se torna muito centrado em aplicativos,
    • cada um ao lado da (s) implementação (ões) primária (s), removendo a vantagem de não precisar recompilar os dependentes quando as dependências mudarem, ou
    • um ou dois em montagens altamente coesas, que aumentam a contagem de montagens, aumentam drasticamente o tempo de "compilação completa" e diminuem o desempenho do aplicativo.

    Tudo isso, novamente para resolver um problema em locais onde pode não haver nenhum.

KeithS
fonte
1
SRP = princípio de responsabilidade única, para quem quiser saber.
Theodore Murdock
1
Sim, e LSP é o princípio da substituição de Liskov; dada uma dependência em A, a dependência deve poder ser atendida por B, que deriva de A sem nenhuma outra alteração.
22412 KeithS
a injeção de dependência ajuda especificamente onde você precisa obter a classe A da classe B da classe D etc., nesse caso (e somente nesse caso), a estrutura DI pode gerar menos inchaço na montagem do que a injeção man de pobre. Também eu nunca tive um gargalo causado pelo DI .. acho que o custo de manutenção, para nunca mais custo de CPU porque o código sempre pode ser otimizado, mas fazer isso sem uma razão tem um custo
GameDeveloper
Outra suposição seria "se uma dependência muda, muda em todos os lugares"? Ou então você tem que olhar para todas as classes de consumo de qualquer maneira
Richard Tingle
3
Esta resposta falha ao abordar o impacto da testabilidade da injeção de dependências em vez de codificá-las. Veja também a explícita Princípio Dependências ( deviq.com/explicit-dependencies-principle )
ssmith
30

Fora das estruturas de injeção de dependência, a injeção de dependência (por injeção de construtor ou injeção de 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 A deve agora também construa o objeto B.

Você reduziu levemente 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 às dependências de A também.

Portanto, a injeção de dependência (sem uma estrutura) é igualmente prejudicial, pois é útil.

No entanto, o custo extra é facilmente justificável: se o código do cliente sabe mais sobre como construir a dependência do que o próprio objeto, 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 de construtores de um fluxo de entrada é a solução óbvia.

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

Pode ser útil, mas você definitivamente deve se perguntar para cada dependência, o benefício do teste vale o custo? Será que realmente quero 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 à 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 facilmente em qual implementação você está confiando e mudando a responsabilidade de decidir em que dependência você está confiando em algum processo de resolução automatizado (por exemplo, se exigirmos um Foo @ Inject'ed , 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, pode haver 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 fazê-lo acaba, novamente, sendo muito semelhante ao custo de fazê-lo: você não precisa saber quem está fornecendo os dados nos quais confia e, se houver vários potenciais fornecedores, você não precisa saber a ordem preferida para procurar fornecedores, verifique se todos os locais que precisam dos dados verificam todos os fornecedores em potencial etc., porque tudo isso é tratado em alto nível pela injeção de dependência plataforma.

Embora eu pessoalmente não tenha muita experiência com estruturas de DI, minha impressão é de que elas oferecem mais benefício que custo, quando a dor de cabeça de encontrar o provedor correto dos dados ou serviços necessários 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 incorretos que causaram uma falha posterior no 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 tenham provado seu valor) quando as estruturas de DI apareceram em cena, e as estruturas de DI foram adotadas porque ofereciam alguma vantagem competitiva, como exigir menos código padrão ou potencialmente fazendo menos para ocultar o provedor de dependência quando for necessário determinar qual provedor está realmente em uso.

Theodore Murdock
fonte
6
Apenas um comentário rápido sobre a simulação de dependência nos testes e "você precisa saber sobre as dependências das dependências de suas dependências" ... Isso não é verdade. Não é necessário conhecer ou se preocupar com as dependências de algumas implementações concretas se um mock estiver sendo injetado. É necessário apenas conhecer as dependências diretas da classe em teste, que são satisfeitas pelos simulados.
Eric King
6
Você entende mal, eu não estou falando sobre quando você injeta uma farsa, eu estou falando sobre o código real. Considere a classe A com dependência B, que por sua vez tem dependência C, que por sua vez tem dependência D. Sem DI, A constrói B, B constrói C, C constrói D. Com injeção de construção, para construir A, você deve primeiro construir B, para construir B, você deve primeiro construir C, e para construir C, você deve primeiro construir D. Portanto, a classe A agora precisa conhecer D, a dependência de uma dependência de uma dependência, para construir B. Isso leva a um acoplamento excessivo.
Theodore Murdock
1
Não haveria muito custo em ter um construtor extra usado apenas para teste que permita que as dependências sejam injetadas. Vou tentar revisar o que disse.
Theodore Murdock
3
O dose da injeção de dependência move apenas as dependências em um jogo de soma zero. Você move suas dependências para lugares que elas já existem: o projeto principal. Você pode até usar abordagens baseadas em convenções para garantir que as dependências sejam resolvidas apenas em tempo de execução, por exemplo, especificando quais classes são concretas por namespaces ou o que for. Eu tenho que dizer que esta é uma resposta ingênua
Sprague
2
@Sprague A injeção de dependência move-se pelas dependências em um jogo de soma ligeiramente negativa, se você a aplicar nos casos em que não deve ser aplicada. O objetivo desta resposta é lembrar às pessoas que a injeção de dependência não é inerentemente boa, é um padrão de design incrivelmente útil nas circunstâncias certas, mas um custo injustificável em circunstâncias normais (pelo menos nos domínios de programação em que trabalhei , Eu entendo que em alguns domínios é mais útil do que em outros). Às vezes, o DI se torna uma palavra da moda e um fim em si mesmo, o que não leva em consideração.
Theodore Murdock
11
  1. se você estiver criando entidades de banco de dados, deve ter alguma classe de fábrica que injetará no seu controlador,

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

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

E não use o Localizador de serviço como alternativa à injeção de dependência, é antipadrão, mais informações: http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx

0lukasz0
fonte
Todos os pontos são sobre o uso de diferentes formas de injeção, em vez de não usá-las completamente. +1 para o link.
Jan Hudec 23/09
3
O Localizador de serviço NÃO é um antipadrão e o cara ao qual você está vinculado não é especialista em padrões. O fato de ele ser famoso pelo StackExchange é uma espécie de desserviço ao design de software. Martin Fowler (autor de Padrões básicos do Enterprise Application Architecture) tem uma visão mais razoável sobre o assunto: martinfowler.com/articles/injection.html
Colin
1
@ Colin, pelo contrário, o Service Locator é definitivamente um antipadrão, e aqui está, em poucas palavras, a razão disso .
Kyralessa 29/07
@ Kyralessa - você percebe que as estruturas de DI usam internamente um padrão do Localizador de Serviço para procurar serviços, certo? Existem circunstâncias em que ambos são apropriados e nenhum é um padrão antiparasitário, apesar do StackExchange odiar que algumas pessoas desejam despejar.
Colin
Claro, e meu carro usa internamente pistões e velas de ignição e várias outras coisas que não são, no entanto, uma boa interface para um ser humano comum que apenas quer dirigir um carro.
Kyralessa
8

Quando você não ganha nada, torna seu projeto sustentável e testável.

Sério, eu amo IoC e DI em geral, e eu diria que 98% das vezes usarei esse padrão sem falhas. É especialmente importante em um ambiente multiusuário, onde o código pode ser reutilizado repetidamente por diferentes membros da equipe e projetos diferentes, pois separa a lógica da implementação. O log é um excelente exemplo disso, uma interface ILog injetada em uma classe é mil vezes mais sustentável do que simplesmente conectar sua estrutura de log-du-jour, pois você não tem garantia de que outro projeto usará a mesma estrutura de log (se usar 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 () na classe Program é outro exemplo) simplesmente não podem ter dependências injetadas na inicialização Tempo. Eu também chegaria ao ponto de dizer que um protótipo, ou qualquer pedaço de código investigativo descartável, também é um candidato ruim; os benefícios do DI são basicamente de médio a longo prazo (testabilidade e manutenção), se você tiver certeza de que jogará fora a maioria de um pedaço de código dentro de uma semana ou mais, eu diria que você não ganha nada isolando dependências, gaste o tempo que você normalmente gasta testando e isolando dependências para que o código funcione.

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

Uma coisa a observar é o seu comentário sobre o teste automatizado: minha definição é de testes funcionais automatizados , por exemplo, testes de selênio com script, se você estiver em um contexto da web. Geralmente, são testes de caixa preta completamente, sem a necessidade de conhecer o funcionamento interno do código. Se você estivesse se referindo a testes de unidade ou integração, eu diria que o padrão DI é quase sempre aplicável a qualquer projeto que dependa fortemente desse tipo de teste de caixa branca, pois, por exemplo, ele permite testar coisas como métodos que tocam no banco de dados sem a necessidade de um banco de dados estar presente.

Ed James
fonte
Não entendo o que você quer dizer com log. Certamente, todos os criadores de logs não estão em conformidade com a mesma interface, portanto, você teria que escrever seu próprio invólucro para traduzir entre a maneira de registrar esse projeto e um criador de logs específico (supondo que você queira alterar facilmente os criadores de log). Então, depois disso, o que DI lhe dá?
precisa
@RichardTingle Eu diria que você deve definir seu wrapper de log como uma interface e depois escrever uma implementação leve dessa interface para cada serviço de log externo, em vez de usar uma única classe de log que agrupe várias abstrações. É o mesmo conceito que você está propondo, mas abstraído em um nível diferente.
Ed James
1

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 existem vários requisitos práticos que precisam ser atendidos para que a introdução da injeção de dependência seja um sucesso.

  1. Deve haver uma razão para introduzi-lo.

    Isso parece óbvio, mas se o seu código apenas obtém as coisas do banco de dados e as devolve sem lógica, a adição de um contêiner de DI torna as coisas mais complexas sem benefícios reais. O teste de integração seria mais importante aqui.

  2. A equipe precisa ser treinada e a bordo.

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

    Se o DI for apresentado por um novo membro da equipe, porque ele entende e gosta e só quer mostrar que é bom, E a equipe não está ativamente envolvida, há um risco real de que realmente diminua a qualidade do o código.

  3. Você precisa testar

    Embora a dissociação 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.
    (Está claro em seu teste que você faz, mas muitas equipes não testam na medida exigida pelo DI.)

timtim
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 período (como um aplicativo Web), o DI pode ser bom.

Quando você tem um aplicativo que inicia muitas vezes e é executado por períodos mais curtos (como um aplicativo móvel), provavelmente não deseja o contêiner.

Koray Tugay
fonte
Eu não ver o tempo como ao vivo de aplicação relacionado com DI
Michael Freidgeim
@MichaelFreidgeim Leva tempo para inicializar o contexto, geralmente os contêineres DI são bastante pesados, como o Spring. Faça um aplicativo hello world com apenas uma classe e faça um com o Spring e inicie as 10 vezes e você verá o que quero dizer.
Koray Tugay
Parece um problema com um contêiner DI individual. No mundo .Net eu não ouvi sobre o tempo de inicialização como um problema essencial para recipientes DI
Michael Freidgeim
1

Tente usar os princípios básicos de POO: use herança para extrair funcionalidades comuns, encapsular (ocultar) coisas que devem ser protegidas do mundo exterior usando tipos / membros privados / internos / protegidos. Use qualquer estrutura de teste poderosa para injetar código apenas para testes, por exemplo, https://www.typemock.com/ ou https://www.telerik.com/products/mocking.aspx .

Em seguida, tente reescrevê-lo com DI e compare o código, o que você verá normalmente com DI:

  1. Você tem mais interfaces (mais tipos)
  2. Você criou duplicatas de assinaturas de métodos públicos e terá que mantê-lo dobrado (você não pode simplesmente alterar algum parâmetro uma vez, precisará fazê-lo duas vezes, basicamente todas as possibilidades de refatoração e navegação se tornam mais complicadas)
  3. Você moveu alguns erros de compilação para executar falhas de tempo (com o DI, você pode simplesmente ignorar alguma dependência durante a codificação e não ter certeza de que será exposta durante o teste)
  4. Você abriu seu encapsulamento. Agora membros protegidos, tipos internos etc tornaram-se públicos
  5. A quantidade geral de código foi aumentada

Eu diria que pelo que vi, quase sempre, a qualidade do código está diminuindo com o DI.

No entanto, se você usar apenas modificador de acesso "público" na declaração de classe e / ou modificadores público / privado para membros, e / ou não tiver a opção de comprar ferramentas de teste caras e ao mesmo tempo precisar de testes de unidade que possam " Não seja substituído por testes integracionais e / ou você já tenha interfaces para as classes que pensa injetar, o DI é uma boa escolha!

ps provavelmente receberei muitas desvantagens para este post, acredito que porque a maioria dos desenvolvedores modernos simplesmente não entende como e por que usar palavras-chave internas e como diminuir o acoplamento de seus componentes e, finalmente, por que diminuí-lo) finalmente, apenas tente codificar e comparar

Eugene Gorbovoy
fonte
0

Uma alternativa à injeção de dependência é usar um localizador de serviço . Um localizador de serviço é mais fácil de entender, depurar e simplificar a construção de um objeto, especialmente se você não estiver usando uma estrutura de DI. Os Localizadores de serviço são um bom padrão para gerenciar dependências estáticas externas , por exemplo, um banco de dados que você teria que passar para cada objeto em sua camada de acesso a dados.

Ao refatorar o código legado , geralmente é mais fácil refatorar para um Localizador de Serviço do que para Injeção de Dependência. Tudo o que você faz é substituir as instanciações pelas pesquisas de serviço e depois falsificar o serviço no seu teste de unidade.

No entanto, existem algumas desvantagens no localizador de serviço. Conhecer as despandâ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 implementações diferentes do mesmo serviço é difícil ou impossível.

TLDR : se sua classe possui dependências estáticas ou você está refatorando o código legado, um Localizador de Serviço é sem dúvida melhor que o DI.

Garrett Hall
fonte
12
O localizador de serviço oculta dependências no seu código . Não é um bom padrão para usar.
Kyralessa
Quando se trata de dependências estáticas, prefiro ver fachadas que implementam interfaces. Estes podem ser injetados por dependência e caem na granada estática para você.
Erik Dietrich
@ Kyralessa Eu concordo, um localizador de serviço tem muitas desvantagens e o DI é quase sempre preferível. No entanto, acredito que há algumas exceções à regra, como em todos os princípios de programação.
Garrett Hall
O principal uso aceito do local de serviço está dentro de um padrão de Estratégia, em que uma classe "selecionadora de estratégia" recebe lógica suficiente para encontrar a estratégia a ser usada e devolvê-la ou passar uma chamada para ela. Mesmo nesse caso, o selecionador de estratégia pode ser uma fachada para um método de fábrica fornecido por um contêiner de IoC, que recebe a mesma lógica; a razão pela qual você divulgou é colocar a lógica onde ela pertence e não onde está mais oculta.
21412 KeithS
Para citar Martin Fowler, "A escolha entre (DI e Service Locator) é menos importante que o princípio de separar a configuração do uso". A idéia de que DI é SEMPRE melhor é uma loucura. Se um serviço é usado globalmente, é mais complicado e quebradiço usar o DI. Além disso, o DI é menos favorável para arquiteturas de plugins em que as implementações não são conhecidas até o tempo de execução. Pense em como seria estranho sempre passar argumentos adicionais para Debug.WriteLine () e similares!
Colin