A injeção de dependência (DI) é um padrão bem conhecido e moderno. A maioria dos engenheiros conhece suas vantagens, como:
- Tornando possível / fácil o isolamento em testes de unidade
- Definindo explicitamente dependências de uma classe
- Facilitar o bom design ( princípio de responsabilidade única (SRP), por exemplo)
- Habilitando a implementação de alternância rapidamente (em
DbLogger
vez deConsoleLogger
por exemplo)
Eu acho que existe um consenso em todo o setor de que o DI é um padrão bom e útil. Não há muitas críticas no momento. As desvantagens mencionadas na comunidade são geralmente menores. Alguns deles:
- Maior número de classes
- Criação de interfaces desnecessárias
Atualmente, discutimos o projeto de arquitetura com meu colega. Ele é bastante conservador, mas de mente aberta. Ele gosta de questionar coisas que considero boas, porque muitas pessoas em TI apenas copiam a tendência mais recente, repetem as vantagens e, em geral, não pensam muito - não analisam muito profundamente.
As coisas que eu gostaria de perguntar são:
- Devemos usar injeção de dependência quando temos apenas uma implementação?
- Deveríamos proibir a criação de novos objetos, exceto os de linguagem / estrutura?
- Injetar uma única implementação é uma péssima idéia (digamos que temos apenas uma implementação para não querermos criar uma interface "vazia") se não planejamos testar uma unidade em particular em uma classe específica?
UserService
classe que é apenas um detentor de lógica. Ele recebe uma conexão com o banco de dados e os testes são executados dentro de uma transação que é revertida. Muitos chamariam isso de má prática, mas descobri que isso funciona muito bem. Não é necessário contorcer seu código apenas para teste e você obtém o erro ao encontrar o poder dos testes de integração.Respostas:
Primeiro, gostaria de separar a abordagem de design do conceito de frameworks. A injeção de dependência em seu nível mais simples e mais fundamental é simplesmente:
É isso aí. Observe que nada disso exige interfaces, estruturas, qualquer estilo de injeção, etc. Para ser justo, aprendi sobre esse padrão pela primeira vez há 20 anos. Isso não é novo.
Devido ao fato de mais de duas pessoas terem confusão sobre o termo pai e filho, no contexto da injeção de dependência:
A injeção de dependência é um padrão para a composição do objeto .
Por que interfaces?
As interfaces são um contrato. Eles existem para limitar o quão bem dois objetos podem ser acoplados. Nem toda dependência precisa de uma interface, mas elas ajudam na escrita de código modular.
Quando você adiciona o conceito de teste de unidade, pode ter duas implementações conceituais para qualquer interface: o objeto real que você deseja usar em seu aplicativo e o objeto zombado ou stub que você usa para testar o código que depende do objeto. Isso por si só pode ser justificativa suficiente para a interface.
Por que estruturas?
Essencialmente, inicializar e fornecer dependências para objetos filhos pode ser assustador quando há um grande número deles. As estruturas fornecem os seguintes benefícios:
Eles também têm as seguintes desvantagens:
Tudo isso dito, existem compensações. Para pequenos projetos em que não há muitas partes móveis e há poucas razões para usar uma estrutura de DI. No entanto, para projetos mais complicados, onde já existem determinados componentes para você, a estrutura pode ser justificada.
Que tal [artigo aleatório na Internet]?
Que tal isso? Muitas vezes as pessoas podem ficar com excesso de zelo e adicionar um monte de restrições e repreendê-lo se você não estiver fazendo as coisas da "maneira única". Não existe um caminho verdadeiro. Veja se você pode extrair algo útil do artigo e ignorar as coisas com as quais não concorda.
Em resumo, pense por si mesmo e experimente as coisas.
Trabalhando com "cabeças velhas"
Aprenda o máximo que puder. O que você encontrará com muitos desenvolvedores que estão trabalhando nos anos 70 é que eles aprenderam a não ser dogmático sobre muitas coisas. Eles têm métodos com os quais trabalham há décadas que produzem resultados corretos.
Tive o privilégio de trabalhar com alguns deles, e eles podem fornecer um feedback brutalmente honesto que faz muito sentido. E, quando vêem valor, acrescentam essas ferramentas ao seu repertório.
fonte
A injeção de dependência é, como a maioria dos padrões, uma solução para os problemas . Então comece perguntando se você tem o problema em primeiro lugar. Caso contrário, o uso do padrão provavelmente tornará o código pior .
Considere primeiro se você pode reduzir ou eliminar dependências. Sendo todas as outras coisas iguais, queremos que cada componente de um sistema tenha o menor número possível de dependências. E se as dependências se foram, a questão de injetar ou não se torna discutível!
Considere um módulo que baixa alguns dados de um serviço externo, analisa e executa algumas análises complexas e grava para resultar em um arquivo.
Agora, se a dependência do serviço externo for codificada, será realmente difícil testar o processamento interno deste módulo. Portanto, você pode decidir injetar o serviço externo e o sistema de arquivos como dependências da interface, o que permitirá injetar zombarias em vez disso, o que, por sua vez, torna possível o teste de unidade da lógica interna.
Mas uma solução muito melhor é simplesmente separar a análise da entrada / saída. Se a análise for extraída para um módulo sem efeitos colaterais, será muito mais fácil testar. Observe que a simulação é um cheiro de código - nem sempre é evitável, mas, em geral, é melhor se você puder testar sem depender da simulação. Portanto, ao eliminar as dependências, você evita os problemas que o DI deve aliviar. Observe que esse design também adere muito melhor ao SRP.
Quero enfatizar que o DI não necessariamente facilita o SRP ou outros bons princípios de design, como separação de preocupações, alta coesão / baixo acoplamento e assim por diante. Pode muito bem ter o efeito oposto. Considere uma classe A que usa outra classe B internamente. B é usado apenas por A e, portanto, totalmente encapsulado e pode ser considerado um detalhe de implementação. Se você alterar isso para injetar B no construtor de A, expôs esse detalhe da implementação e agora tem conhecimento sobre essa dependência e sobre como inicializar B, a vida útil de B e assim por diante devem existir em algum outro lugar do sistema separadamente de A. Então, você tem uma arquitetura geral pior com preocupações de vazamento.
Por outro lado, existem alguns casos em que o DI é realmente útil. Por exemplo, para serviços globais com efeitos colaterais como um criador de logs.
O problema é quando padrões e arquiteturas se tornam objetivos em si mesmos, e não ferramentas. Apenas perguntando "Devemos usar DI?" é como colocar a carroça diante do cavalo. Você deve perguntar: "Temos algum problema?" e "Qual é a melhor solução para esse problema?"
Uma parte de sua pergunta se resume a: "Devemos criar interfaces supérfluas para satisfazer as demandas do padrão?" Você provavelmente já percebe a resposta para isso - absolutamente não ! Qualquer pessoa que diga o contrário está tentando vender algo para você - provavelmente horas de consultoria caras. Uma interface só tem valor se representar uma abstração. Uma interface que apenas imita a superfície de uma única classe é chamada de "interface de cabeçalho" e esse é um antipadrão conhecido.
fonte
A
usadoB
na produção, mas somente tiver sido testadoMockB
, nossos testes não nos dizem se funcionará na produção. Quando componentes puros (sem efeito colateral) de um modelo de domínio estão injetando e zombando uns dos outros, o resultado é um enorme desperdício de tempo de todos, uma base de código inchada e frágil e baixa confiança no sistema resultante. Zombe dos limites do sistema, não entre partes arbitrárias do mesmo sistema.Na minha experiência, há uma série de desvantagens na injeção de dependência.
Primeiro, o uso do DI não simplifica os testes automatizados tanto quanto os anunciados. O teste de unidade de uma classe com uma implementação simulada de uma interface permite validar como essa classe irá interagir com a interface. Ou seja, permite que você teste de unidade como a classe em teste usa o contrato fornecido pela interface. No entanto, isso oferece uma garantia muito maior de que a entrada da classe em teste na interface é a esperada. Ele fornece uma garantia bastante fraca de que a classe em teste responde conforme o esperado para a saída da interface, pois é uma saída quase universalmente simulada, que está sujeita a bugs, simplificações excessivas e assim por diante. Em resumo, NÃO permite validar que a classe se comportará conforme o esperado com uma implementação real da interface.
Segundo, o DI torna muito mais difícil navegar pelo código. Ao tentar navegar para a definição de classes usadas como entrada para funções, uma interface pode ser qualquer coisa, desde um pequeno aborrecimento (por exemplo, onde existe uma única implementação) a um grande tempo gasto (por exemplo, quando uma interface excessivamente genérica como IDisposable é usada) ao tentar encontrar a implementação real sendo usada. Isso pode transformar um exercício simples como "Preciso corrigir uma exceção de referência nula no código que ocorre logo após a impressão dessa instrução de log" em um esforço de um dia.
Terceiro, o uso de DI e estruturas é uma faca de dois gumes. Pode reduzir bastante a quantidade de código da placa da caldeira necessária para operações comuns. No entanto, isso custa às custas de conhecimento detalhado da estrutura de DI específica para entender como essas operações comuns são realmente conectadas. Compreender como as dependências são carregadas na estrutura e adicionar uma nova dependência na estrutura para injetar pode exigir a leitura de uma quantidade razoável de material de plano de fundo e a seguir alguns tutoriais básicos sobre a estrutura. Isso pode transformar algumas tarefas simples em tarefas bastante demoradas.
fonte
Eu segui o conselho de Mark Seemann em "Injeção de dependência no .NET" - para supor.
O DI deve ser usado quando você tem uma 'dependência volátil', por exemplo, há uma chance razoável de que isso mude.
Portanto, se você acha que pode ter mais de uma implementação no futuro ou a implementação pode mudar, use o DI. Caso contrário,
new
está bem.fonte
Minha maior preocupação sobre DI já foi mencionada em algumas respostas de uma maneira passageira, mas vou expandir um pouco aqui. DI (como é feito principalmente hoje, com contêineres etc.) realmente prejudica a legibilidade do código. E a legibilidade do código é sem dúvida a razão por trás da maioria das inovações de programação atuais. Como alguém disse - escrever código é fácil. Ler código é difícil. Mas também é extremamente importante, a menos que você esteja escrevendo algum tipo de pequeno utilitário descartável de gravação única.
O problema com o DI nesse sentido é que é opaco. O contêiner é uma caixa preta. Os objetos simplesmente aparecem de algum lugar e você não tem idéia - quem os construiu e quando? O que foi passado para o construtor? Com quem estou compartilhando esta instância? Quem sabe...
E quando você trabalha principalmente com interfaces, todos os recursos de "ir para a definição" do seu IDE ficam em chamas. É muito difícil rastrear o fluxo do programa sem executá-lo e apenas avançar para ver apenas qual implementação da interface foi usada neste local específico. E, ocasionalmente, há algum obstáculo técnico que impede você de avançar. E mesmo que você possa, se isso envolve atravessar as entranhas retorcidas do contêiner de DI, todo o caso rapidamente se torna um exercício de frustração.
Para trabalhar eficientemente com um pedaço de código que usou DI, você deve estar familiarizado com ele e já saber o que vai aonde.
fonte
Embora o DI em geral seja certamente uma coisa boa, sugiro não usá-lo cegamente para tudo. Por exemplo, nunca injeto registradores. Uma das vantagens do DI é tornar as dependências explícitas e claras. Não faz sentido listar
ILogger
como dependência de quase todas as classes - é apenas desorganização. É de responsabilidade do registrador fornecer a flexibilidade necessária. Todos os meus registradores são membros finais estáticos, posso considerar injetar um registrador quando precisar de um não estático.Essa é uma desvantagem da estrutura de DI fornecida ou da estrutura de zombaria, não da própria DI. Na maioria dos lugares, minhas aulas dependem de aulas concretas, o que significa que não há necessidade de clichê. O Guice (uma estrutura Java DI) vincula, por padrão, uma classe a si próprio e eu só preciso substituir a ligação nos testes (ou conectá-los manualmente).
Eu só crio as interfaces quando necessário (o que é bastante raro). Isso significa que, às vezes, tenho que substituir todas as ocorrências de uma classe por uma interface, mas o IDE pode fazer isso por mim.
Sim, mas evite qualquer clichê adicional .
Não. Haverá muitas classes de valor (imutável) e de dados (mutável), em que as instâncias são criadas e transmitidas e onde não há sentido em injetá-las - pois elas nunca são armazenadas em outro objeto (ou apenas em outros objetos). tais objetos).
Para eles, talvez seja necessário injetar uma fábrica, mas na maioria das vezes não faz sentido (imagine, por exemplo
@Value class NamedUrl {private final String name; private final URL url;}
; você realmente não precisa de uma fábrica aqui e não há nada a injetar).IMHO está bem, desde que não cause inchaço no código. Injete a dependência, mas não crie a interface (e nenhum XML de configuração maluco!), Pois você pode fazê-lo mais tarde, sem qualquer aborrecimento.
Na verdade, no meu projeto atual, existem quatro classes (entre centenas), que decidi excluir do DI , pois são classes simples usadas em muitos lugares, incluindo objetos de dados.
Outra desvantagem da maioria das estruturas de DI é a sobrecarga do tempo de execução. Isso pode ser movido para o tempo de compilação (para Java, há Dagger , nenhuma idéia sobre outras linguagens).
Outra desvantagem é a mágica que está acontecendo em todos os lugares, que pode ser diminuída (por exemplo, desativei a criação de proxy ao usar o Guice).
fonte
Devo dizer que, na minha opinião, toda a noção de injeção de dependência é superestimada.
DI é o equivalente moderno dos valores globais. As coisas que você está injetando são singletons globais e objetos de código puro; caso contrário, você não pode injetá-los. A maioria dos usos de DI são forçados a você para usar uma determinada biblioteca (JPA, Spring Data, etc). Na maioria das vezes, o DI fornece o ambiente perfeito para nutrir e cultivar espaguete.
Com toda a honestidade, a maneira mais fácil de testar uma classe é garantir que todas as dependências sejam criadas em um método que possa ser substituído. Em seguida, crie uma classe de teste derivada da classe real e substitua esse método.
Então você instancia a classe Test e testa todos os seus métodos. Isso não ficará claro para alguns de vocês - os métodos que você está testando são os da classe em teste. E todos esses testes de método ocorrem em um único arquivo de classe - a classe de teste de unidade associada à classe em teste. Não há custos indiretos aqui - é assim que o teste de unidade funciona.
No código, esse conceito se parece com isso ...
O resultado é exatamente equivalente ao uso do DI - ou seja, o ClassUnderTest está configurado para teste.
As únicas diferenças são que esse código é totalmente conciso, completamente encapsulado, mais fácil de codificar, mais fácil de entender, mais rápido, usa menos memória, não requer uma configuração alternativa, não requer estruturas, nunca será a causa de uma página 4 (WTF!) Rastreio de pilha que inclui exatamente as classes ZERO (0) que você escreveu e é completamente óbvio para qualquer pessoa com o menor conhecimento de OO, do iniciante ao Guru (você pensaria, mas estaria enganado).
Dito isto, é claro que não podemos usá-lo - é muito óbvio e não está na moda o suficiente.
No final do dia, porém, minha maior preocupação com o DI é a dos projetos que vi falhar miseravelmente, todos eles têm bases de código maciças onde o DI era a cola que mantinha tudo unido. O DI não é uma arquitetura - é realmente relevante apenas em algumas situações, a maioria das quais são impostas a você para usar outra biblioteca (JPA, Spring Data, etc.). Na maioria das vezes, em uma base de código bem projetada, a maioria dos usos de DI ocorreria em um nível abaixo do local em que suas atividades diárias de desenvolvimento acontecem.
fonte
bar
só podem ser chamados depoisfoo
, é uma API ruim. Ele está reivindicando fornecer a funcionalidade (bar
), mas não podemos realmente usá-la (poisfoo
pode não ter sido chamada). Se você deseja manter seuinitializeDependencies
padrão (anti?), Deve pelo menos torná-lo privado / protegido e chamá-lo automaticamente do construtor, para que a API seja sincera.Realmente sua pergunta se resume a "O teste de unidade é ruim?"
99% de suas classes alternativas para injetar serão simulações que permitem o teste de unidade.
Se você realizar testes de unidade sem DI, terá o problema de como fazer com que as classes usem dados simulados ou um serviço simulado. Vamos dizer "parte da lógica", pois você pode não estar separando-a em serviços.
Existem maneiras alternativas de fazer isso, mas o DI é bom e flexível. Depois de testá-lo, você é praticamente forçado a usá-lo em qualquer lugar, pois você precisa de algum outro código, mesmo o chamado 'DI do pobre homem' que instancia os tipos concretos.
É difícil imaginar uma desvantagem tão ruim que a vantagem do teste de unidade seja superada.
fonte
new
em um método, sabemos que o mesmo código em execução na produção estava sendo executado durante os testes.Order
exemplo é um teste de unidade: está testando um método (ototal
método deOrder
). Você está reclamando que está chamando código de 2 classes, minha resposta é e daí ? Não estamos testando "2 classes de uma vez", estamos testando ototal
método. Não devemos nos importar como um método faz seu trabalho: esses são detalhes de implementação; testá-los causa fragilidade, acoplamento rígido, etc. Apenas nos preocupamos com o comportamento do método (valor de retorno e efeitos colaterais), não com as classes / módulos / registradores de CPU / etc. usado no processo.