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?
design-patterns
dependency-injection
Tom Squires
fonte
fonte
Respostas:
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
new
e 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 isso, novamente para resolver um problema em locais onde pode não haver nenhum.
fonte
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.
fonte
se você estiver criando entidades de banco de dados, deve ter alguma classe de fábrica que injetará no seu controlador,
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.
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
fonte
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.
fonte
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.
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.
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.
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.)
fonte
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.
fonte
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:
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
fonte
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.
fonte