Qual é a maneira "correta" de implementar a DI no .NET?


22

Estou procurando implementar injeção de dependência em um aplicativo relativamente grande, mas não tenho experiência nele. Estudei o conceito e algumas implementações de IoC e injetores de dependência disponíveis, como Unity e Ninject. No entanto, há uma coisa que está me iludindo. Como devo organizar a criação da instância no meu aplicativo?

O que estou pensando é que posso criar algumas fábricas específicas que conterão a lógica de criar objetos para alguns tipos de classe específicos. Basicamente, uma classe estática com um método que chama o método Ninject Get () de uma instância estática do kernel nesta classe.

Será uma abordagem correta da implementação da injeção de dependência no meu aplicativo ou devo implementá-lo de acordo com outro princípio?


5
Não acho que exista o caminho certo, mas muitos caminhos certos, dependendo do seu projeto. Eu aderiria aos outros e sugeriria a injeção de construtores, pois você poderá garantir que todas as dependências sejam injetadas em um único ponto. Além disso, se as assinaturas do construtor crescerem demais, você saberá que as classes fazem muito.
Paul Kertscher,

É difícil responder sem saber que tipo de projeto .net você está criando. Uma boa resposta para o WPF pode ser uma resposta ruim para o MVC, por exemplo.
JMK

É bom organizar todos os registros de dependência em um módulo de DI para a solução ou para cada projeto, e possivelmente um para alguns testes, dependendo da profundidade que você deseja testar. Ah, sim, é claro que você deve usar injeção de construtor, o outro é para usos mais avançados / malucos.
Mark Rogers

Respostas:


30

Não pense ainda sobre a ferramenta que você irá usar. Você pode fazer DI sem um contêiner de IoC.

Primeiro ponto: Mark Seemann tem um livro muito bom sobre DI em .Net

Segundo: raiz da composição. Certifique-se de que toda a configuração seja feita no ponto de entrada do projeto. O restante do seu código deve saber sobre injeções, não sobre qualquer ferramenta que esteja sendo usada.

Terceiro: a injeção de construtor é o caminho mais provável (existem casos em que você não gostaria, mas não muitos).

Quarto: examine o uso de fábricas lambda e outros recursos semelhantes para evitar a criação de interfaces / classes desnecessárias com o único objetivo de injeção.


5
Todos excelentes conselhos; especialmente a primeira parte: aprenda a fazer DI puro e comece a examinar os contêineres IoC que podem reduzir a quantidade de código padrão exigido por essa abordagem.
David Arno

6
... ou pule todos os contêineres de IoC - para manter todos os benefícios da verificação estática.
Den

Gosto que esse conselho seja um bom ponto de partida, antes de entrar no DI, e na verdade tenho o livro de Mark Seemann.
Snoop

Eu posso secundar esta resposta. Utilizamos com êxito o pobre-homem-DI (bootstrapper escrito à mão) em um aplicativo bastante grande, separando partes da lógica do bootstrapper nos módulos que compuseram o aplicativo.
Wigy

1
Seja cuidadoso. O uso de injeções de lambda pode ser um caminho rápido para a loucura, especialmente do tipo de dano de projeto induzido pelo teste. Eu sei. Eu segui esse caminho.
Jpmc26 27/06/16

13

Há duas partes em sua pergunta - como implementar o DI corretamente e como refatorar um aplicativo grande para usar o DI.

A primeira parte é bem respondida por @Miyamoto Akira (especialmente a recomendação de ler o livro "injeção de dependência em .net" de Mark Seemann. O blog Marks também é um bom recurso gratuito.

A segunda parte é muito mais complicada.

Um bom primeiro passo seria simplesmente mover toda a instanciação para os construtores de classes - não injetando as dependências, apenas certificando-se de chamar apenas newo construtor.

Isso destacará todas as violações do SRP que você está cometendo, para que você possa começar a dividir a classe em colaboradores menores.

O próximo problema que você encontrará serão as classes que dependem dos parâmetros de tempo de execução para construção. Geralmente, você pode corrigir isso criando fábricas simples, geralmente Func<param,type>inicializando-as no construtor e chamando-as nos métodos.

O próximo passo seria criar interfaces para suas dependências e adicionar um segundo construtor às suas classes, exceto essas interfaces. Seu construtor sem parâmetros renovaria as instâncias concretas e as passaria para o novo construtor. Isso é comumente chamado de 'Injeção B * stard' ou 'Pobre homem DI'.

Isso permitirá que você faça alguns testes de unidade e, se esse foi o objetivo principal do refator, pode ser onde você para. O novo código será gravado com a injeção do construtor, mas o código antigo pode continuar funcionando como gravado, mas ainda pode ser testado.

Você pode, é claro, ir além. Se você pretende usar um contêiner IOC, a próxima etapa pode ser substituir todas as chamadas diretas para newseus construtores sem parâmetros por chamadas estáticas para o contêiner IOC, essencialmente (ab) usando-o como um localizador de serviço.

Isso exibirá mais casos de parâmetros do construtor de tempo de execução para lidar como antes.

Feito isso, você pode começar a remover os construtores sem parâmetros e refatorar para o DI puro.

Em última análise, isso vai dar muito trabalho, portanto, decida por que você quer fazê-lo e priorize as partes da base de código que serão mais beneficiadas pelo refator


3
Obrigado por uma resposta muito elaborada. Você me deu algumas idéias sobre como abordar o problema que estou enfrentando. Toda a arquitetura do aplicativo já foi criada com a IoC em mente. O principal motivo pelo qual desejo usar o DI não é nem um teste de unidade, mas um bônus, mas uma capacidade de trocar diferentes implementações por diferentes interfaces, definidas no núcleo do meu aplicativo, com o mínimo de esforço possível. O aplicativo em questão funciona em um ambiente em constante mudança e geralmente tenho que trocar partes do aplicativo para usar novas implementações de acordo com as mudanças no ambiente.
user3223738

1
Ainda bem que pude ajudar, e sim, eu concordo que a maior vantagem do DI é o acoplamento solto e a capacidade de reconfigurar facilmente isso traz, com o teste de unidade sendo um bom efeito colateral.
27416 Steve

1

Primeiro, quero mencionar que você está tornando isso muito mais difícil refatorando um projeto existente, em vez de iniciar um novo projeto.

Você disse que é um aplicativo grande, então escolha um pequeno componente para começar. De preferência, um componente 'leaf-node' que não é usado por mais nada. Não sei qual é o estado dos testes automatizados neste aplicativo, mas você estará quebrando todos os testes de unidade desse componente. Então esteja preparado para isso. A etapa 0 é escrever testes de integração para o componente que você modificará se eles já não existirem. Como último recurso (sem infraestrutura de teste; não há necessidade de escrevê-la), descubra uma série de testes manuais que você pode fazer para verificar se esse componente está funcionando.

A maneira mais simples de declarar sua meta para o refator de DI é que você deseja remover todas as instâncias do operador 'novo' desse componente. Eles geralmente se enquadram em duas categorias:

  1. Variável de membro invariável: são variáveis ​​definidas uma vez (geralmente no construtor) e não são reatribuídas para a vida útil do objeto. Para estes, você pode injetar uma instância do objeto no construtor. Você geralmente não é responsável por descartar esses objetos (não quero dizer nunca aqui, mas você realmente não deveria ter essa responsabilidade).

  2. Variável de membro variante / variável de método: são variáveis ​​que serão coletadas como lixo em algum momento durante a vida útil do objeto. Para isso, você deve injetar uma fábrica em sua classe para fornecer essas instâncias. Você é responsável por descartar objetos criados por uma fábrica.

Seu contêiner de IoC (por mais que pareça) será responsável por instanciar esses objetos e implementar suas interfaces de fábrica. O que quer que esteja usando o componente que você modificou precisará saber sobre o contêiner de IoC para que ele possa recuperar seu componente.

Depois de concluir o acima, você poderá colher todos os benefícios que espera obter da DI no componente selecionado. Agora seria um bom momento para adicionar / corrigir esses testes de unidade. Se houver testes de unidade existentes, você terá que decidir se deseja consertá-los injetando objetos reais ou escrever novos testes de unidade usando zombarias.

'Simplesmente' repita o procedimento acima para cada componente do seu aplicativo, movendo a referência para o contêiner de IoC conforme você avança até que apenas as principais necessidades sejam conhecidas.


1
Bom conselho: comece com um pequeno componente em vez da 'reescrita GRANDE' #
Daniel Hollinrake

0

A abordagem correta é usar injeção de construtor, se você usar

O que estou pensando é que posso criar algumas fábricas específicas que conterão a lógica de criar objetos para alguns tipos de classe específicos. Basicamente, uma classe estática com um método que chama o método Ninject Get () de uma instância estática do kernel nesta classe.

então você acaba com o localizador de serviço, em vez de injeção de dependência.


Certo. Injeção de construtor. Digamos que eu tenho uma classe que está aceitando uma implementação de interface como um dos argumentos. Mas ainda preciso criar uma instância da implementação da interface e passá-la para esse construtor em algum lugar. De preferência, deve ser um pedaço de código centralizado.
user3223738

2
Não, quando você inicializa o contêiner DI, é necessário especificar a implementação de interfaces, e o contêiner DI cria a instância e injeta no construtor.
Pelicano de vôo baixo

Pessoalmente, acho que a injeção de construtor é usada em excesso. Vi com muita frequência que 10 serviços diferentes são injetados e apenas um é realmente necessário para uma chamada de função - por que não faz parte do argumento da função?
urbanhusky

2
Se 10 serviços diferentes forem injetados, é porque alguém está violando o SRP, que deve ser dividido em componentes menores.
Pelicano de vôo baixo

1
@ Fabio A questão é o que isso compraria. Ainda estou para ver um exemplo em que ter uma classe gigante que lida com uma dúzia de coisas totalmente diferentes é um bom design. A única coisa que a DI faz é tornar todas essas violações mais óbvias.
Voo

0

Você diz que deseja usá-lo, mas não indica o porquê.

O DI nada mais é do que fornecer um mecanismo para gerar concreções a partir de interfaces.

Isso por si só vem do DIP . Se o seu código já está escrito nesse estilo e você tem um único local onde as concreções são geradas, o DI não traz mais nada para a festa. A adição de código de estrutura DI aqui seria simplesmente ofuscar e ofuscar sua base de código.

Supondo que você não quiser usá-lo, você normalmente criada a fábrica / Construtor / recipiente (ou qualquer outro) no início do aplicativo para que ele seja claramente visível.

NB, é muito fácil criar o seu próprio, caso deseje, em vez de se comprometer com o Ninject / StructureMap ou o que seja. Se, no entanto, você tiver uma rotatividade razoável de pessoal, poderá lubrificar as rodas para usar uma estrutura reconhecida ou, pelo menos, escrevê-la nesse estilo para que não seja uma curva de aprendizado muito grande.


0

Na verdade, o caminho "certo" é NÃO usar uma fábrica, a menos que não exista outra opção (como nos testes de unidade e em certas simulações - para o código de produção, você NÃO usa uma fábrica)! Fazer isso é realmente um antipadrão e deve ser evitado a todo custo. O objetivo principal de um contêiner de DI é permitir que o gadget faça o trabalho por você.

Conforme mencionado acima em uma postagem anterior, você deseja que seu gadget de IoC assuma a responsabilidade pela criação dos vários objetos dependentes no seu aplicativo. Isso significa deixar seu gadget de DI criar e gerenciar as várias instâncias em si. Esse é o ponto principal da DI - seus objetos NUNCA devem saber como criar e / ou gerenciar os objetos dos quais dependem. Fazer o contrário quebra o acoplamento solto.

Converter um aplicativo existente em todas as DIs é um grande passo, mas deixando de lado as óbvias dificuldades em fazê-lo, você também desejará (apenas para facilitar a sua vida) explorar uma ferramenta de DIs que executará automaticamente a maior parte de suas ligações (o núcleo de algo como o Ninject são as "kernel.Bind<someInterface>().To<someConcreteClass>()"chamadas que você faz para corresponder suas declarações de interface às classes concretas que deseja usar para implementar essas interfaces. São aquelas chamadas "Vinculadas" que permitem que seu gadget DI intercepte suas chamadas de construtor e forneça o instâncias de objeto dependentes necessárias.Um construtor típico (pseudo-código mostrado aqui) para alguma classe pode ser:

public class SomeClass
{
  private ISomeClassA _ClassA;
  private ISomeOtherClassB _ClassB;

  public SomeClass(ISomeClassA aInstanceOfA, ISomeOtherClassB aInstanceOfB)
  {
    if (aInstanceOfA == null)
      throw new NullArgumentException();
    if (aInstanceOfB == null)
      throw new NullArgumentException();
    _ClassA = aInstanceOfA;
    _ClassB = aInstanceOfB;
  }

  public void DoSomething()
  {
    _ClassA.PerformSomeAction();
    _ClassB.PerformSomeOtherActionUsingTheInstanceOfClassA(_ClassA);
  }
}

Observe que em nenhum lugar desse código havia qualquer código que criava / gerenciava / liberava a instância de SomeConcreteClassA ou SomeOtherConcreteClassB. De fato, nenhuma classe concreta foi sequer referenciada. Então ... onde a mágica aconteceu?

Na parte de inicialização do seu aplicativo, ocorreu o seguinte (novamente, esse é um pseudo-código, mas é bem parecido com o real (Ninject) ...):

public void StartUp()
{
  kernel.Bind<ISomeClassA>().To<SomeConcreteClassA>();
  kernel.Bind<ISomeOtherClassB>().To<SomeOtherConcreteClassB>();
}

Esse pouco de código diz ao dispositivo Ninject para procurar construtores, varrê-los, procurar instâncias de interfaces que foram configuradas para lidar (que são as chamadas "Bind") e depois criar e substituir uma instância da classe concreta em qualquer lugar a instância é referenciada.

Existe uma boa ferramenta que complementa muito bem o Ninject, chamado Ninject.Extensions.Conventions (outro pacote do NuGet), que fará a maior parte deste trabalho para você. Para não tirar a excelente experiência de aprendizado que você experimentará ao criar isso sozinho, mas para começar, isso pode ser uma ferramenta para investigar.

Se a memória servir, o Unity (formalmente da Microsoft agora um projeto de código aberto) possui uma ou duas chamadas de método que fazem a mesma coisa, outras ferramentas têm ajudantes semelhantes.

Qualquer que seja o caminho que você escolher, leia definitivamente o livro de Mark Seemann para a maior parte do seu treinamento em DI, no entanto, deve-se salientar que mesmo os "Grandes" do mundo da engenharia de software (como Mark) podem cometer erros flagrantes - Mark esqueceu tudo. Ninject em seu livro, então aqui está outro recurso escrito apenas para Ninject. Eu tenho isso e é uma boa leitura: Dominando o Ninject para injeção de dependência


0

Não existe um "caminho certo", mas existem alguns princípios simples a seguir:

  • Crie a raiz da composição na inicialização do aplicativo
  • Após a criação da raiz da composição, jogue fora a referência para o contêiner DI / kernel (ou pelo menos encapsule-a para que não seja diretamente acessível a partir do seu aplicativo)
  • Não crie instâncias via "new"
  • Passe todas as dependências necessárias como abstração para o construtor

Isso é tudo. Certamente, esses são princípios, não leis, mas se você os seguir, poderá ter certeza de que está fazendo DI (por favor, corrija-me se eu estiver errado).


Então, como criar objetos durante o tempo de execução sem "novo" e sem conhecer o contêiner de DI?

No caso do NInject, há uma extensão de fábrica que fornece a criação de fábricas. Claro, as fábricas criadas ainda têm uma referência interna ao kernel, mas isso não é acessível a partir do seu aplicativo.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.