Eu gostaria de ter certeza de que entendi o conceito de injeção de dependência (DI). Bem, eu realmente entendo o conceito, DI não é complicado: você cria uma interface e passa a implementação da minha interface para a classe que a usa. A maneira comum de transmiti-lo é por construtor, mas você também pode transmiti-lo por setter ou outro método.
O que não tenho certeza de entender é quando usar o DI.
Uso 1: É claro que usar DI, caso você tenha várias implementações de sua interface, parece lógico. Você tem um repositório para o SQL Server e outro para o banco de dados Oracle. Ambos compartilham a mesma interface e você "injeta" (este é o termo usado) o que você deseja no tempo de execução. Isso nem é DI, é a programação básica do OO aqui.
Uso 2: Quando você tem uma camada de negócios com muitos serviços com todos os métodos específicos, parece que a boa prática é criar uma interface para cada serviço e também injetar a implementação, mesmo que essa seja única. Porque isso é melhor para manutenção. Esse é o segundo uso que não entendo.
Eu tenho algo como 50 classes de negócios. Nada é comum entre eles. Alguns são repositórios que obtêm ou salvam dados em 3 bancos de dados diferentes. Alguns leem ou escrevem arquivos. Alguns fazem pura ação comercial. Existem também validadores e auxiliares específicos. O desafio é o gerenciamento de memória, porque algumas classes são instanciadas a partir de locais diferentes. Um validador pode chamar vários repositórios e outros validadores que podem chamar os mesmos repositórios novamente.
Exemplo: camada de negócios
public class SiteService : Service, ICrud<Site>
{
public Site Read(Item item, Site site)
{
return beper4DbContext.Site
.AsNoTracking()
.SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
}
public Site Read(string itemCode, string siteCode)
{
using (var itemService = new ItemService())
{
var item = itemService.Read(itemCode);
return Read(item, site);
}
}
}
public class ItemSiteService : Service, ICrud<Site>
{
public ItemSite Read(Item item, Site site)
{
return beper4DbContext.ItemSite
.AsNoTracking()
.SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
}
public ItemSite Read(string itemCode, string siteCode)
{
using (var itemService = new ItemService())
using (var siteService = new SiteService())
{
var item = itemService.Read(itemCode);
var site = siteService.Read(itemCode, siteCode);
return Read(item, site);
}
}
}
Controlador
public class ItemSiteController : BaseController
{
[Route("api/Item/{itemCode}/ItemSite/{siteCode}")]
public IHttpActionResult Get(string itemCode, string siteCode)
{
using (var service = new ItemSiteService())
{
var itemSite = service.Read(itemCode, siteCode);
return Ok(itemSite);
}
}
}
Este exemplo é muito básico, mas você vê como eu posso criar facilmente duas instâncias de itemService para obter um itemSite. Além disso, cada serviço também vem com seu contexto de banco de dados. Portanto, esta chamada criará 3 DbContext. 3 Conexões.
Minha primeira idéia foi criar um singleton para reescrever todo esse código como abaixo. O código é mais legível e mais importante: o sistema singleton cria apenas uma instância de cada serviço usado e cria na primeira chamada. Perfeito, exceto que ainda tenho contextos diferentes, mas posso fazer o mesmo sistema para meus contextos. Tão feito.
Camada de negócios
public class SiteService : Service, ICrud<Site>
{
public Site Read(Item item, Site site)
{
return beper4DbContext.Site
.AsNoTracking()
.SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
}
public Site Read(string itemCode, string siteCode)
{
var item = ItemService.Instance.Read(itemCode);
return Read(item, site);
}
}
public class ItemSiteService : Service, ICrud<Site>
{
public ItemSite Read(Item item, Site site)
{
return beper4DbContext.ItemSite
.AsNoTracking()
.SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
}
public ItemSite Read(string itemCode, string siteCode)
{
var item = ItemService.Instance.Read(itemCode);
var site = SiteService.Instance.Read(itemCode, siteCode);
return Read(item, site);
}
}
Controlador
public class ItemSiteController : BaseController
{
[Route("api/Item/{itemCode}/ItemSite/{siteCode}")]
public IHttpActionResult Get(string itemCode, string siteCode)
{
var itemSite = service.Instance.Read(itemCode, siteCode);
return Ok(itemSite);
}
}
Algumas pessoas me dizem, de acordo com as boas práticas, que eu deveria usar o DI com instância única e usar o singleton é uma prática ruim. Eu devo criar uma interface para cada classe de negócios e instancia-la com a ajuda do contêiner de DI. Mesmo? Este DI simplifica meu código. Difícil de acreditar.
fonte
Respostas:
O caso de uso mais "popular" para DI (além do uso do padrão de "estratégia" que você já descreveu) é provavelmente o teste de unidade.
Mesmo que você pense que haverá apenas uma implementação "real" para uma interface injetada, no caso de você fazer testes de unidade, geralmente há uma segunda: uma implementação "simulada" com o único objetivo de tornar possível um teste isolado. Isso oferece o benefício de não ter que lidar com a complexidade, os possíveis erros e talvez o impacto no desempenho do componente "real".
Portanto, não, o DI não é para aumentar a legibilidade, é usado para aumentar a testabilidade (- é claro, não exclusivamente).
Este não é um fim em si mesmo. Se a sua classe
ItemService
é muito simples, o que não facilita o acesso a redes ou bancos de dados externos, portanto, não impede a gravação de testes de unidade para algo comoSiteService
, testar o último isoladamente pode valer a pena, portanto o DI não será necessário. No entanto, seItemService
estiver acessando outros sites pela rede, você provavelmente desejaráSiteService
separar o teste de unidade , o que pode ser feito substituindo o "real"ItemService
por umMockItemService
, que fornece alguns itens falsos codificados.Deixe-me salientar outra coisa: em seus exemplos, alguém pode argumentar que não será necessário o DI aqui para testar a lógica de negócios principal - os exemplos mostram sempre duas variantes dos
Read
métodos, uma com a lógica de negócios real envolvida (que pode ser testada sem DI) e uma que é apenas o código de "cola" para conectar aItemService
lógica anterior. No caso mostrado, esse é realmente um argumento válido contra o DI - de fato, se o DI puder ser evitado sem sacrificar a testabilidade dessa maneira, vá em frente. Mas nem todo código do mundo real é tão simples, e muitas vezes o DI é a solução mais simples para obter testabilidade de unidade "suficiente".fonte
Ao não usar a injeção de dependência, você se permite criar conexões permanentes com outros objetos. Conexões que você pode esconder dentro de onde elas irão surpreender as pessoas. Conexões que eles só podem mudar reescrevendo o que você está criando.
Em vez disso, você pode usar a injeção de dependência (ou aprovação de referência, se você é um veterano como eu) para tornar explícito o que um objeto precisa, sem forçá-lo a definir como deve ser atendido.
Isso força você a aceitar muitos parâmetros. Mesmo aqueles com padrões óbvios. Em C #, você tem sorte e argumentos opcionais . Isso significa que você tem argumentos padrão. Se você não se importa de estar estaticamente vinculado aos seus padrões, mesmo quando não os usa, é possível permitir o DI sem ficar sobrecarregado com as opções. Isto segue a convenção sobre a configuração .
Testar não é uma boa justificativa para DI. No momento em que você pensa que alguém lhe venderá uma estrutura de zombaria do wiz bang que usa reflexão ou alguma outra mágica para convencê-lo de que você pode voltar à maneira como trabalhou antes e usar a magia para fazer o resto.
Quando usado corretamente, o teste pode ser uma boa maneira de mostrar se um projeto é isolado. Mas esse não é o objetivo. Isso não impede que os vendedores tentem provar que, com magia suficiente, tudo fica isolado. Mantenha a mágica no mínimo.
O objetivo desse isolamento é gerenciar as mudanças. É bom se uma alteração puder ser feita em um só lugar. Não é bom ter que segui-lo arquivo após arquivo, esperando que a loucura termine.
Coloque-me em uma loja que se recusa a fazer testes de unidade e eu ainda vou fazer DI. Faço isso porque me permite separar o que é necessário de como é feito. Testando ou não testando, quero esse isolamento.
fonte
A visão de helicóptero do DI é simplesmente a capacidade de trocar uma implementação por uma interface . Embora isso, obviamente, seja uma benção para o teste, existem outros benefícios em potencial:
Implementações de versão de um objeto
Se seus métodos aceitam parâmetros de interface nas camadas intermediárias, você pode passar as implementações que desejar na camada superior, o que reduz a quantidade de código que precisa ser escrita para trocar as implementações. É certo que, de qualquer maneira, esse é um benefício das interfaces, mas se o código for escrito com o DI em mente, você obterá esse benefício imediatamente.
Reduzindo o número de objetos que precisam passar por camadas
Embora isso se aplique principalmente às estruturas DI, se o objeto A exigir uma instância do objeto B , é possível consultar o kernel (ou qualquer outra coisa) para gerar o objeto B em tempo real, em vez de passá-lo pelas camadas. Isso reduz a quantidade de código que precisa ser gravada e testada. Ele também mantém as camadas que não se importam com o objeto B limpas.
fonte
Não é necessário usar interfaces para usar DI. O principal objetivo do DI é separar a construção e o uso de objetos.
O uso de singletons é justamente desaprovado na maioria dos casos. Uma das razões é que fica muito difícil obter uma visão geral das dependências de uma classe.
No seu exemplo, o ItemSiteController poderia simplesmente usar um ItemSiteService como um argumento construtor. Isso permite evitar qualquer custo de criação de objetos, mas evita a inflexibilidade de um singleton. O mesmo vale para ItemSiteService, se ele precisar de um ItemService e um SiteService, injete-os no construtor.
O benefício é maior quando todos os objetos usam injeção de dependência. Isso permite centralizar a construção em um módulo dedicado ou delegá-lo em um contêiner de DI.
Uma hierarquia de dependência pode se parecer com isso:
Observe que há apenas uma classe sem parâmetros de construtor e apenas uma interface. Ao configurar o contêiner de DI, você pode decidir qual armazenamento usar, ou se o armazenamento em cache deve ser usado etc. O teste é mais fácil, pois você pode decidir em qual banco de dados usar ou usar algum outro tipo de armazenamento. Você também pode configurar o contêiner de DI para tratar objetos como singletons, se necessário, dentro do contexto do objeto de contêiner.
fonte
Você isola sistemas externos.
Sim, use DI aqui. Se estiver indo para a rede, banco de dados, sistema de arquivos, outro processo, entrada do usuário etc. Você deseja isolá-lo.
O uso do DI facilitará o teste, pois você zombará facilmente desses sistemas externos. Não, não estou dizendo que esse é o primeiro passo para o teste de unidade. Nem que você não possa fazer testes sem fazer isso.
Além disso, mesmo se você tivesse apenas um banco de dados, o uso do DI ajudaria no dia em que você deseja migrar. Então, sim, DI.
Claro, o DI pode ajudá-lo. Eu debateria sobre os contêineres.
Talvez, algo digno de nota, seja que a injeção de dependência com tipos de concreto ainda seja injeção de dependência. O que importa é que você pode criar instâncias personalizadas. Não precisa ser injeção de interface (embora a injeção de interface seja mais versátil, isso não significa que você deve usá-la em qualquer lugar).
A idéia de criar uma interface explícita para cada classe tem que morrer. De fato, se você tivesse apenas uma implementação de uma interface ... YAGNI . Adicionar uma interface é relativamente barato, pode ser feito quando você precisar. Na verdade, eu sugiro esperar até que você tenha duas ou três implementações candidatas para ter uma idéia melhor do que as coisas são comuns entre elas.
No entanto, o outro lado disso é que você pode criar interfaces que correspondam mais perto do que o código do cliente precisa. Se o código do cliente precisar apenas de alguns membros de uma classe, você poderá ter uma interface apenas para isso. Isso levará a uma melhor separação de interface .
Recipientes?
Você sabe que não precisa deles.
Vamos reduzi-lo às compensações. Há casos em que não valem a pena. Sua classe terá as dependências necessárias no construtor. E isso pode ser bom o suficiente.
Eu realmente não sou fã de atributos de anotação para "injeção de setter", muito menos de terceiros, entendo que pode ser necessário para implementações fora do seu controle ... no entanto, se você decidir alterar a biblioteca, isso precisará mudar.
Eventualmente, você começará a criar rotinas para criar esses objetos, porque, para criá-lo, primeiro será necessário criar esses outros e, para aqueles que você precisar, um pouco mais ...
Bem, quando isso acontece, você deseja colocar toda essa lógica em um único local e reutilizá-la. Você deseja uma única fonte de verdade sobre como criar seu objeto. E você entende isso por não se repetir . Isso simplificará seu código. Faz sentido, certo?
Bem, onde você coloca essa lógica? O primeiro instinto será ter um localizador de serviço . Uma implementação simples é um singleton com um dicionário de fábricas somente leitura . Uma implementação mais complexa pode usar a reflexão para criar as fábricas quando você não tiver fornecido uma.
No entanto, o uso de um localizador de serviço único ou estático significa que você estará fazendo algo como
var x = IoC.Resolve<?>
todos os locais em que precisar criar uma instância. O que está adicionando um forte acoplamento ao localizador / contêiner / injetor de serviço. Isso pode realmente tornar o teste de unidade mais difícil.Você quer um injetor que instancia e o mantenha apenas para ser usado no controlador. Você não quer que ele seja aprofundado no código. Isso poderia realmente tornar os testes mais difíceis. Se alguma parte do seu código precisar instanciar algo, deverá esperar uma instância (ou quase uma fábrica) em seu construtor.
E se você está tendo muitos parâmetros no construtor ... veja se você tem parâmetros que viajam juntos. Provavelmente, você pode mesclar parâmetros em tipos de descritor (tipos de valor idealmente).
fonte