Quando devemos usar injeção de dependência (C #) [fechado]

8

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.

Bastien Vandamme
fonte
4
DI nunca foi sobre legibilidade.
Robert Harvey

Respostas:

14

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 como SiteService, testar o último isoladamente pode valer a pena, portanto o DI não será necessário. No entanto, se ItemServiceestiver acessando outros sites pela rede, você provavelmente desejará SiteServiceseparar o teste de unidade , o que pode ser feito substituindo o "real" ItemServicepor um MockItemService, 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 Readmé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 a ItemServiceló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".

Doc Brown
fonte
1
Eu realmente entendo, mas e se eu não fizer testes de unidade. Faço testes de integração com um banco de dados falso. As mudanças são operadas com muita frequência para manter testes de unidade e nossos dados de produção são alterados o tempo todo. Portanto, testar com cópia de dados reais é a única maneira de testar seriamente. No lugar dos testes de unidade, protegemos todos os nossos métodos com contratos de código. Sei que isso não é o mesmo e conheço o teste de unidade em um ponto positivo, mas não tenho tempo para priorizar o teste de unidade. Isso é um fato, não uma escolha.
Bastien Vandamme
1
@BastienVandamme: existe um grande espaço entre "nenhum teste de unidade" e "nós testamos tudo". E nunca vi um projeto enorme que não conseguisse encontrar alguns benefícios no uso de testes de unidade, pelo menos em algumas partes. Identifique essas peças e verifique se o DI é necessário para torná-las testáveis ​​na unidade.
Doc Brown
4

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.

candied_orange
fonte
2
Você pode explicar por que o teste não é uma boa justificativa para o DI? Você parece confundi-lo com estruturas de reflexão mágica - e eu entendo não gostar delas -, mas não faz nenhum contraponto à própria idéia.
Jacob Raihle 01/07/19
1
O argumento de zombaria parece uma ladeira escorregadia. Não uso estruturas de simulação, mas entendo os benefícios do DI.
Robert Harvey
2
É apenas uma cauda de advertência. Quando as pessoas pensam que a habilitação de testes automatizados é a única razão para realizar a DI, elas não apenas perdem o entendimento dos benefícios da flexibilidade de design, como também se tornam vulneráveis ​​aos campos de vendas de produtos que prometem benefícios sem o trabalho. DI é trabalho. Não há almoço grátis.
Candied_orange 01/07/19
3
Em outras palavras, a dissociação é a verdadeira razão da DI. O teste é simplesmente uma das áreas mais comuns em que os benefícios da dissociação são evidentes.
StackOverthrow
2
Ei, não é brincadeira. Se o DI não ajudar a manter seu código, não o use.
Candied_orange 02/07/19
2

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.

Robbie Dee
fonte
2

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:

public interface IStorage
{
}

public class DbStorage : IStorage
{
    public DbStorage(string connectionString){}
}

public class FileStorage : IStorage
{
    public FileStorage(FileInfo file){}
}

public class MemoryStorage : IStorage
{
}

public class CachingStorage : IStorage
{
    public CachingStorage(IStorage storage) { }
}

public class MyService
{
    public MyService(IStorage storage){}
}

public class Controller
{
    public Controller(MyService service){}
}

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.

JonasH
fonte
"No seu exemplo, o ItemSiteController poderia simplesmente pegar um ItemSiteService como um argumento construtor." Obviamente, mas um controlador pode usar de 1 a 10 serviços. Em algum momento, posso adicionar um novo serviço, forçando-me a alterar a assinatura do construtor. Como você pode dizer que isso é melhor para manutenção? E não, isso provavelmente me custará criar o objeto várias vezes, porque eu não tenho mais o mecanismo singleton. Então eu vou ter que passar para cada controlador usando. Como posso garantir que qualquer desenvolvedor nem sempre o recrie. Qual sistema pode garantir que eu o crie apenas uma vez?
Bastien Vandamme
Eu acho que, como a maioria das pessoas, você está misturando o padrão DI com o Recurso Recipiente. Seu exemplo é o meu uso 1. Não desafio este. Quando você possui várias implementações de uma interface, deve usar o que chama DI. Eu chamo isso de OOP. Tanto faz. Desafio o Uso 2, onde a maioria das pessoas parece usá-lo para testes e manutenção. Entendo, mas isso não resolve minha solicitação de instância única. Espero que o Container pareça oferecer uma solução. Container ... não apenas DI. Eu acho que preciso de DI com um recurso de instância única e posso encontrá-lo em contêineres (bibliotecas de terceiros).
Bastien Vandamme
@ bastien-vandamme Se você precisar adicionar novos serviços, adicione-os ao construtor, isso não deve ser um problema. Se você precisar injetar muitos serviços, isso pode indicar um problema com a arquitetura, não a idéia de injetar as dependências. Você garante que a mesma instância seja reutilizada por outros desenvolvedores, desenvolvendo e treinando seus colegas. Mas, em geral, deve ser possível ter várias instâncias de uma classe. Como exemplo, eu posso ter várias instâncias de banco de dados e ter um serviço para cada uma.
JonasH 02/07/19
Euh não. Eu tenho um banco de dados com 50 tabelas, preciso de 50 repositórios, 50 serviços. Posso trabalhar com repositórios genéricos, mas meu banco de dados está longe de ser normalizado e limpo; portanto, qualquer que seja o repositório ou serviço em algum momento, deve ter um código específico por causa do histórico. Então, eu não posso fazer tudo genérico. Cada serviço tem regras comerciais específicas que preciso manter separadamente.
Bastien Vandamme
2

Você isola sistemas externos.

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.


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.

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.

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).

Theraot
fonte
Obrigado por esta explicação clara. Portanto, de acordo com o seu, devo criar uma classe de injetor que contenha a lista de todos os meus serviços. Eu não injeto serviço por serviço. Eu injeto minha classe de injetor. Esta classe gerencia também a instância única de cada um dos meus serviços?
Bastien Vandamme
@BastienVandamme, você pode acoplar seus controladores ao injetor. Então, se você quer passar para o controlador, eu concordo. E sim, ele pode lidar com uma única instância dos serviços. Por outro lado, eu me preocuparia se o controlador estivesse passando o injetor ... ele pode passar pelas fábricas, se necessário. De fato, a idéia é separar os serviços da inicialização de suas dependências. Portanto, idealmente, os serviços não estão cientes do injetor.
Theraot 02/07/19
"O uso do DI facilitará o teste, porque você zombará facilmente desses sistemas externos". Descobri que confiar em zombar desses sistemas em testes de unidade sem primeiro minimizar o código que possui qualquer conexão com eles reduz severamente a utilidade de realizar testes de unidade. Primeiro, maximize a quantidade de código que pode ser testado apenas através do teste de entrada / saída. Em seguida, verifique se a zombaria é necessária.
precisa saber é
@ jpmc26 concordou. Adendo: devemos isolar esses sistemas externos de qualquer maneira.
Theraot
@BastienVandamme Eu estive pensando sobre isso. Eu acho que, se você não perder a capacidade de criar para criar injetores personalizados para distribuir, isso deve ser bom.
Theraot 02/07/19