Crítica e desvantagens da injeção de dependência

109

Injeção de dependência (DI) é um padrão bem conhecido e elegante. A maioria dos engenheiros conhece suas vantagens, como:

  • Possibilitando o isolamento no teste de unidade / fácil
  • Definindo explicitamente as dependências de uma classe
  • Facilitando o bom design ( princípio da responsabilidade única (SRP), por exemplo)
  • Ativando implementações de comutação rapidamente ( DbLogger em vez de ConsoleLogger , por exemplo)

Eu acredito que há um amplo consenso na indústria de que a DI é um bom e útil padrão. Não há muitas críticas no momento. Desvantagens que são mencionadas na comunidade são geralmente menores. Alguns deles:

  • Maior número de aulas
  • Criação de interfaces desnecessárias

Atualmente, discutimos o design da arquitetura com meu colega. Ele é bastante conservador, mas de mente aberta. Ele gosta de questionar coisas, o que eu considero bom, porque muitas pessoas na área de 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 a injeção de dependência quando temos apenas uma implementação?
  • Deveríamos proibir a criação de novos objetos, exceto os de linguagem / framework?
  • Está injetando uma única ideia ruim de implementação (digamos que temos apenas uma implementação para não criarmos interface "vazia") se não planejamos testar uma classe em particular?
por Landeeyo 29.05.2018 / 13:20
fonte

8 respostas

152

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 fundamental é simplesmente:

A parent object provides all the dependencies required to the child object.

É isso. Note que nada disso requer interfaces, frameworks, qualquer estilo de injeção, etc. Para ser justo, aprendi sobre esse padrão há 20 anos. Não é novo.

Devido a mais de 2 pessoas terem confusão sobre o termo pai e filho, no contexto da injeção de dependência:

  • O pai é o objeto que instancia e configura o objeto filho que ele usa
  • O filho é o componente projetado para ser passivamente instanciado. Ou seja Ele é projetado para usar quaisquer dependências fornecidas pelo pai e não instancia suas próprias dependências.

Injeção de dependência é um padrão para objeto composição .

Por que interfaces?

Interfaces são um contrato. Eles existem para limitar o quão bem acoplados dois objetos podem ser. Nem toda dependência precisa de uma interface, mas ajudam a escrever códigos modulares.

Quando você adiciona o conceito de teste de unidade, você pode ter duas implementações conceituais para qualquer interface: o objeto real que você deseja usar em seu aplicativo e o objeto ridicularizado ou stubbed que você usa para testar o código que depende do objeto. Só isso pode ser justificativa suficiente para a interface.

Por que os frameworks

Inicialmente, a inicialização e o fornecimento de dependências para objetos filho podem ser assustadores quando há um grande número deles. Os frameworks fornecem os seguintes benefícios:

  • Dependências de autowiring para componentes
  • Configurando os componentes com configurações de algum tipo
  • Automatizando o código da placa de caldeira para que você não precise vê-lo escrito em vários locais.

Eles também têm as seguintes desvantagens:

  • O objeto pai é um "contêiner" e não há nada em seu código
  • Torna os testes mais complicados se você não puder fornecer as dependências diretamente em seu código de teste
  • Pode retardar a inicialização, pois resolve todas as dependências usando reflexão e muitos outros truques
  • Depuração de tempo de execução pode ser mais difícil, particularmente se o contêiner injetar um proxy entre a interface e o componente real que implementa a interface (programação orientada a aspectos integrada no Spring vem à mente). O contêiner é uma caixa preta e nem sempre é construído com qualquer conceito de facilitar o processo de depuração.

Tudo o que disse, há trade-offs. Para projetos pequenos, onde 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 existem certos componentes já feitos para você, a estrutura pode ser justificada.

E sobre [artigo aleatório na Internet]?

E 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 está fazendo as coisas do "único caminho verdadeiro". Não há um caminho verdadeiro. Veja se você pode extrair algo útil do artigo e ignorar as coisas que você não concorda.

Em suma, pense por si mesmo e tente as coisas.

Trabalhando com "cabeças antigas"

Aprenda o máximo que puder. O que você vai encontrar com muitos desenvolvedores que estão trabalhando em seus 70 anos é que eles aprenderam a não ser dogmáticos sobre muitas coisas. Eles têm métodos com os quais trabalham há décadas que produzem resultados corretos.

Eu tive o privilégio de trabalhar com alguns deles, e eles podem fornecer um feedback brutalmente honesto que faz muito sentido. E onde eles vêem valor, eles adicionam essas ferramentas ao seu repertório.

    
por 29.05.2018 / 15:03
fonte
80

A injeção de dependência é, como a maioria dos padrões, uma solução para problemas . Então comece perguntando se você tem o problema em primeiro lugar. Se não, então usar o padrão provavelmente tornará o código pior .

Primeiro, considere se você pode reduzir ou eliminar dependências. Todas as outras coisas sendo iguais, queremos que cada componente em um sistema tenha o menor número de dependências possível. E se as dependências desaparecerem, a questão de injetar ou não se torna discutível!

Considere um módulo que faz o download de 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á muito difícil testar o processamento interno deste módulo. Assim, você pode decidir injetar o serviço externo e o sistema de arquivos como dependências de interface, o que permitirá a injeção de mocks, o que, por sua vez, torna possível testar a lógica interna da unidade.

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. Note-se que zombaria é um cheiro de código - nem sempre é evitável, mas, em geral, é melhor se você pode testar sem depender de zombaria. Então, eliminando as dependências, você evita os problemas que a 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 princípios de design bons, como separação de interesses, alta coesão / baixo acoplamento e assim por diante. Poderia também 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, então você expôs este detalhe de implementação e agora o conhecimento sobre essa dependência e sobre como inicializar B, o tempo de vida 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 vazamento de preocupações.

Por outro lado, existem alguns casos em que a DI é realmente útil. Por exemplo, para serviços globais com efeitos colaterais como um registrador.

O problema é quando padrões e arquiteturas se tornam objetivos em si mesmos, em vez de ferramentas. Apenas perguntando "Devemos usar DI?" é tipo de colocar o carrinho antes do cavalo. Você deveria perguntar: "Nós temos um problema?" e "Qual é a melhor solução para esse problema?"

Uma parte da sua pergunta se resume a: "Devemos criar interfaces supérfluas para satisfazer as demandas do padrão?" Você provavelmente já percebeu a resposta para isso - absolutamente não ! Qualquer um que diga o contrário está tentando vender algo - 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.

    
por 29.05.2018 / 15:34
fonte
33

Na minha experiência, há uma série de desvantagens na injeção de dependência.

Primeiro, usar o DI não simplifica os testes automatizados, tanto quanto os anunciados. Unidade de teste de uma classe com uma implementação simulada de uma interface permite validar como essa classe irá interagir com a interface. Ou seja, permite testar como a classe em teste usa o contrato fornecido pela interface. No entanto, isso fornece uma garantia muito maior de que a entrada da classe em teste na interface é a esperada. Ele fornece uma garantia bastante pobre de que a classe em teste responde como esperado para a saída da interface, já que essa é uma saída quase universalmente falsa, que está sujeita a bugs, simplificações excessivas e assim por diante. Em resumo, ele NÃO permite validar que a classe se comportará como esperado com uma implementação real da interface.

Segundo, a 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 incômodo (por exemplo, onde há uma única implementação) até um grande coletor de tempo (por exemplo, quando uma interface excessivamente genérica como IDisposable for usada) ao tentar encontrar a implementação real em uso. Isso pode transformar um exercício simples como "Preciso corrigir uma exceção de referência nula no código que acontece logo após a impressão da declaração de registro" em um esforço diário.

Terceiro, o uso de DI e frameworks é uma faca de dois gumes. Isso pode reduzir bastante a quantidade de código de placa de caldeira necessária para operações comuns. No entanto, isso ocorre em detrimento da necessidade de conhecimento detalhado da estrutura de DI específica para entender como essas operações comuns são realmente conectadas. Entender como as dependências são carregadas na estrutura e adicionar uma nova dependência à estrutura para injetar pode exigir a leitura de uma quantidade razoável de material de apoio e seguir alguns tutoriais básicos sobre a estrutura. Isso pode transformar algumas tarefas simples em demoradas.

    
por 29.05.2018 / 23:44
fonte
12

Eu segui o conselho de Mark Seemann de "Injeção de dependência no .NET" - para supor.

DI deve ser usado quando você tem uma 'dependência volátil', por exemplo há uma chance razoável de que isso possa mudar.

Portanto, se você acha que pode ter mais de uma implementação no futuro ou a implementação pode mudar, use DI. Caso contrário new está bem.

    
por 29.05.2018 / 13:38
fonte
2

Enabling switching implementations quickly (DbLogger instead of ConsoleLogger for example)

Embora a DI, em geral, seja certamente uma coisa boa, sugiro que não a use cegamente para tudo. Por exemplo, eu nunca injeto loggers. 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 desordem. É da responsabilidade do logger fornecer a flexibilidade que você precisa. Todos os meus registradores são membros finais estáticos, posso considerar a injeção de um registrador quando preciso de um não-estático.

Increased number of classes

Esta é uma desvantagem da estrutura de DI ou estrutura de simulação, não da própria DI. Na maioria dos lugares, minhas classes dependem de classes concretas, o que significa que não há clichê zero necessário. Guice (uma estrutura de Java DI) liga por padrão uma classe a si mesma e eu preciso apenas substituir a ligação em testes (ou conectá-los manualmente).

Creation of unnecessary interfaces

Eu apenas crio as interfaces quando necessário (o que é bastante raro). Isso significa que, às vezes, preciso substituir todas as ocorrências de uma classe por uma interface, mas o IDE pode fazer isso por mim.

Should we use dependency injection when we have just one implementation?

Sim, mas evite qualquer clichê adicionado .

Should we ban creating new objects except language/framework ones?

Não. Haverá muitas classes de valor (imutável) e de dados (mutável), onde as instâncias são criadas e passadas e onde não há sentido em injetá-los - como eles nunca são armazenados em outro objeto (ou apenas em outros objetos).

Para eles, você pode precisar 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 para injetar).

Is injecting a single implementation bad idea (let's say we have just one implementation so we don't want to create "empty" interface) if we don't plan to unit test a particular class?

IMHO está tudo bem, desde que não cause nenhum inchaço de código. Injete a dependência, mas não crie a interface (e nenhum XML de configuração maluco!), Como você pode fazer depois, sem qualquer aborrecimento.

Na verdade, no meu projeto atual, existem quatro classes (de centenas), que eu decidi excluir do DI como são classes simples usadas em muitos lugares, incluindo objetos de dados.

Outra desvantagem da maioria das estruturas de DI é a sobrecarga de tempo de execução. Isso pode ser movido para o tempo de compilação (para Java, Dagger , não há idéia sobre outros idiomas).

Outra desvantagem é a mágica acontecendo em todos os lugares, que pode ser desativada (por exemplo, eu desativei a criação de proxy ao usar o Guice).

    
por 01.06.2018 / 11:16
fonte
2

Minha maior implicância com DI já foi mencionada em algumas respostas de uma maneira passageira, mas vou expandir um pouco aqui. DI (como é feito na maior parte hoje, com contêineres etc.) realmente, 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 o código é difícil. Mas também é extremamente importante, a menos que você esteja escrevendo algum tipo de pequeno utilitário write-once throwaway.

O problema com DI a este respeito é que é opaco. O contêiner é uma caixa preta. Objetos simplesmente aparecem de algum lugar e você não tem ideia - 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 traçar o fluxo do programa sem executá-lo e apenas percorrendo 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 percorrer. E mesmo se você puder, se envolver as entranhas torcidas do contêiner DI, todo o caso rapidamente se tornará um exercício de frustração.

Para trabalhar de forma eficiente com um trecho de código que utilizou o DI, você deve estar familiarizado com ele e já saber o que acontece.

    
por 03.06.2018 / 21:15
fonte
-4

Eu tenho que 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 poderia injetá-los. A maioria dos usos de DI é forçada em você para usar uma determinada biblioteca (JPA, Spring Data, etc). Na maioria das vezes, DI fornece o ambiente perfeito para cultivar e cultivar esparguete.

Honestamente, 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.

Em seguida, 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 que pertencem à 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á sobrecarga zero aqui - é assim que o teste de unidade funciona.

No código, esse conceito se parece com isso ...

class ClassUnderTest {

   protected final ADependency;
   protected final AnotherDependency;

   // Call from a factory or use an initializer 
   public void initializeDependencies() {
      aDependency = new ADependency();
      anotherDependency = new AnotherDependency();
   }
}

class TestClassUnderTest extends ClassUnderTest {

    @Override
    public void initializeDependencies() {
      aDependency = new MockitoObject();
      anotherDependency = new MockitoObject();
    }

    // Unit tests go here...
    // Unit tests call base class methods
}

O resultado é exatamente equivalente ao uso de DI - isto é, o ClassUnderTest é configurado para testes.

As únicas diferenças são que este 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 nenhum framework, nunca será a causa de um rastreio de pilha de 4 páginas (WTF!) que inclui exatamente ZERO (0) classes que você escreveu, e é completamente óbvio para qualquer pessoa com o menor conhecimento de OO, do iniciante ao Guru (você poderia pensar, mas estaria enganado).

Dito isto, é claro que não podemos usá-lo - é óbvio demais e não é moda o suficiente.

No final das contas, minha maior preocupação com DI é que os projetos que tenho visto falham miseravelmente, todos eles têm sido bases de código massivas onde DI era a cola que mantinha tudo junto. DI não é uma arquitetura - é realmente relevante apenas em algumas situações, a maioria das quais é forçada em 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 de onde acontecem suas atividades diárias de desenvolvimento.

    
por 30.05.2018 / 05:43
fonte
-6

Realmente, sua pergunta se resume a "O teste de unidade é ruim?"

99% das suas classes alternativas para injetar serão mocks que permitem o teste de unidade.

Se você faz testes unitários sem DI, você tem o problema de como fazer com que as classes usem dados com escárnio ou um serviço escarnecido. Vamos dizer "parte da lógica", como você pode não estar separando em serviços.

Existem maneiras alternativas de fazer isso, mas a DI é boa e flexível. Uma vez que você o tenha testado, você é virtualmente forçado a usá-lo em todos os lugares, já que você precisa de algum outro pedaço de código, até mesmo a chamada 'DI pobre', que instancia os tipos concretos.

É difícil imaginar uma desvantagem tão ruim que a vantagem do teste de unidade esteja sobrecarregada.

    
por 29.05.2018 / 13:36
fonte