Quanto custa muita injeção de dependência?

38

Eu trabalho em um projeto que usa injeção de dependência (Spring) para literalmente tudo o que é uma dependência de uma classe. Estamos em um ponto em que o arquivo de configuração do Spring aumentou para cerca de 4000 linhas. Há pouco tempo, assisti a uma das palestras do tio Bob no YouTube (infelizmente, não consegui encontrar o link), em que ele recomenda injetar apenas algumas dependências centrais (por exemplo, fábricas, banco de dados, etc.) no componente Principal a partir do qual ser distribuído.

As vantagens dessa abordagem são a dissociação da estrutura DI da maior parte do aplicativo, além de tornar a configuração do Spring mais limpa, pois as fábricas conterão muito mais do que estava na configuração anterior. Pelo contrário, isso resultará na disseminação da lógica de criação entre muitas classes de fábrica e os testes poderão se tornar mais difíceis.

Então, minha pergunta é realmente quais são as outras vantagens ou desvantagens que você vê em uma ou outra abordagem. Existem práticas recomendadas? Muito obrigado por suas respostas!

Antimon
fonte
3
Ter um arquivo de configuração de 4000 linhas não é culpa da injeção de dependência ... existem muitas maneiras de corrigi-lo: modularize o arquivo em vários arquivos menores, mude para a injeção baseada em anotação, use arquivos JavaConfig em vez de xml. Basicamente, o problema que você está enfrentando é uma falha no gerenciamento da complexidade e tamanho das necessidades de injeção de dependência do seu aplicativo.
precisa saber é o seguinte
11
O @Maybe_Factor TL; DR dividiu o projeto em componentes menores.
Walfrat 05/09

Respostas:

44

Como sempre, depende. A resposta depende do problema que se está tentando resolver. Nesta resposta, tentarei abordar algumas forças motivadoras comuns:

Favorecer bases de código menores

Se você possui 4.000 linhas de código de configuração do Spring, suponho que a base de código tenha milhares de classes.

Não é um problema que você possa resolver após o fato, mas como regra geral, eu tendem a preferir aplicativos menores, com bases de código menores. Se você gosta de Design Orientado a Domínio , pode, por exemplo, criar uma base de código por contexto limitado.

Baseei esse conselho em minha experiência limitada, pois escrevi código de linha de negócios baseado na Web durante a maior parte da minha carreira. Eu poderia imaginar que, se você estiver desenvolvendo um aplicativo de desktop, um sistema incorporado ou outro, é mais difícil separar as coisas.

Embora perceba que esse primeiro conselho é facilmente o menos prático, também acredito que é o mais importante, e é por isso que o incluo. A complexidade do código varia de maneira não linear (possivelmente exponencialmente) com o tamanho da base de código.

Favor Pure DI

Embora eu ainda perceba que esta pergunta apresenta uma situação existente, recomendo o Pure DI . Não use um contêiner de DI, mas se o fizer, pelo menos use-o para implementar a composição baseada em convenções .

Não tenho nenhuma experiência prática com o Spring, mas estou assumindo que, pelo arquivo de configuração , um arquivo XML está implícito.

Configurar dependências usando XML é o pior dos dois mundos. Primeiro, você perde a segurança do tipo em tempo de compilação, mas não ganha nada. Um arquivo de configuração XML pode facilmente ser do tamanho do código que ele tenta substituir.

Comparado ao problema que ele pretende resolver, os arquivos de configuração da injeção de dependência ocupam o lugar errado no relógio de complexidade da configuração .

O caso da injeção de dependência de granulação grossa

Eu posso defender uma injeção de dependência de granulação grossa. Também posso defender a injeção de dependência refinada (consulte a próxima seção).

Se você injetar apenas algumas dependências 'centrais', a maioria das classes será semelhante a:

public class Foo
{
    private readonly Bar bar;

    public Foo()
    {
        this.bar = new Bar();
    }

    // Members go here...
}

Isso ainda se encaixa na composição de objetos a favor dos Design Patterns sobre a herança de classe , porque Foocompõe Bar. De uma perspectiva de manutenção, isso ainda pode ser considerado sustentável, porque se você precisar alterar a composição, basta editar o código-fonte Foo.

Isso é dificilmente menos sustentável do que a injeção de dependência. Na verdade, eu diria que é mais fácil editar diretamente a classe que usa Bar, em vez de seguir o indireto inerente à injeção de dependência.

Na primeira edição do meu livro sobre injeção de dependência , faço a distinção entre dependências voláteis e estáveis.

Dependências voláteis são aquelas dependências que você deve considerar injetar. Eles incluem

  • Dependências que devem ser reconfiguráveis ​​após a compilação
  • Dependências desenvolvidas em paralelo por outra equipe
  • Dependências com comportamento não determinístico ou comportamento com efeitos colaterais

Dependências estáveis, por outro lado, são dependências que se comportam de maneira bem definida. Em certo sentido, você poderia argumentar que essa distinção justifica a injeção de dependência de granulação grossa, embora deva admitir que não percebi totalmente isso quando escrevi o livro.

De uma perspectiva de teste, no entanto, isso dificulta o teste de unidade. Você não pode mais testar a unidade Fooindependentemente Bar. Como JB Rainsberger explica , os testes de integração sofrem com uma explosão combinatória de complexidade. Você literalmente precisará escrever dezenas de milhares de casos de teste se quiser cobrir todos os caminhos por meio de uma integração de até 4-5 classes.

O contra-argumento disso é que, freqüentemente, sua tarefa não é programar uma classe. Sua tarefa é desenvolver um sistema que resolva alguns problemas específicos. Essa é a motivação por trás do Behavior-Driven Development (BDD).

Outra visão sobre isso é apresentada por DHH, que afirma que o TDD leva a danos no projeto induzidos pelo teste . Ele também é a favor de testes de integração de granulação grossa.

Se você adota essa perspectiva no desenvolvimento de software, a injeção de dependência de granulação grossa faz sentido.

O caso da injeção de dependência refinada

A injeção de dependência refinada, por outro lado, pode ser descrita como injetar todas as coisas!

Minha principal preocupação em relação à injeção de dependência de granulação grossa é a crítica expressa por JB Rainsberger. Você não pode cobrir todos os caminhos de código com testes de integração, porque você precisa escrever literalmente milhares ou dezenas de milhares de casos de teste para cobrir todos os caminhos de código.

Os proponentes do BDD vão contra o argumento de que você não precisa cobrir todos os caminhos de código com testes. Você só precisa cobrir aqueles que produzem valor comercial.

Na minha experiência, no entanto, todos os caminhos de código 'exóticos' também serão executados em uma implantação de alto volume e, se não testados, muitos deles terão defeitos e causarão exceções em tempo de execução (geralmente exceções de referência nula).

Isso me levou a favorecer a injeção de dependência refinada, porque me permite testar os invariantes de todos os objetos isoladamente.

Favorecer a programação funcional

Enquanto eu me inclino para a injeção de dependência refinada, mudei minha ênfase para a programação funcional, entre outros motivos, porque é intrinsecamente testável .

Quanto mais você avançar para o código SOLID, mais funcional ele se tornará . Mais cedo ou mais tarde, você também pode mergulhar. A arquitetura funcional é a arquitetura de portas e adaptadores e a injeção de dependência também é uma tentativa e as portas e adaptadores . A diferença, no entanto, é que uma linguagem como Haskell impõe essa arquitetura por meio de seu sistema de tipos.

Favorecer a programação funcional digitada estaticamente

Neste ponto, desisti essencialmente da programação orientada a objetos (OOP), embora muitos dos problemas da OOP estejam intrinsecamente acoplados a linguagens comuns, como Java e C #, mais do que o próprio conceito.

O problema com as principais linguagens OOP é que é quase impossível evitar o problema de explosão combinatória, que, não testado, leva a exceções em tempo de execução. Linguagens de tipo estático, como Haskell e F #, por outro lado, permitem codificar muitos pontos de decisão no sistema de tipos. Isso significa que, em vez de ter que escrever milhares de testes, o compilador simplesmente dirá se você lidou com todos os caminhos de código possíveis (até certo ponto; não é uma bala de prata).

Além disso, a injeção de dependência não é funcional . A verdadeira programação funcional deve rejeitar toda a noção de dependências . O resultado é um código mais simples.

Sumário

Se forçado a trabalhar com C #, prefiro a injeção de dependência refinada, pois me permite cobrir toda a base de código com um número gerenciável de casos de teste.

No final, minha motivação é um feedback rápido. Ainda assim, o teste de unidade não é a única maneira de obter feedback .

Mark Seemann
fonte
11
Eu realmente gosto desta resposta, e especialmente desta declaração usada no contexto do Spring: "Configurar dependências usando XML é o pior dos dois mundos. Primeiro, você perde a segurança do tipo em tempo de compilação, mas não ganha nada. Uma configuração XML o arquivo pode ser tão grande quanto o código que ele tenta substituir ".
Thomas Carlisle
@ThomasCarlisle não era o ponto da configuração XML que você não precisa tocar no código (nem mesmo compilar) para alterá-lo? Eu nunca (ou mal) o usei por causa das duas coisas que Mark mencionou, mas você ganha algo em troca.
El Mac
1

Grandes classes de configuração de DI são um problema. Mas pense no código que ele substitui.

Você precisa instanciar todos esses serviços e outros enfeites. O código para fazer isso seria no app.main()ponto de partida e injetado manualmente, ou fortemente acoplado como this.myService = new MyService();dentro das classes.

Reduza o tamanho da sua classe de instalação dividindo-a em várias classes de instalação e chame-as a partir do ponto inicial do programa. ie

main()
{
   var c = new diContainer();
   var service1 = diSetupClass.SetupService1(c);
   var service2 = diSetupClass.SetupService2(c, service1); //if service1 is required by service2
   //etc

   //main logic
}

Você não precisa fazer referência ao service1 ou 2, exceto para passá-los para outros métodos de configuração da ci.

É melhor do que tentar obtê-los do contêiner, uma vez que impõe a ordem de chamar as configurações.

Ewan
fonte
11
Para quem deseja aprofundar esse conceito (construindo sua árvore de classes no ponto inicial do programa), o melhor termo a ser pesquisado é "raiz da composição"
e_i_pi
@ Ewan, que civariável é essa ?
superjos 6/09/17
sua classe de instalação ci com métodos estáticos
Ewan
urg deve ser di. Eu continuo pensando em 'container'
Ewan