Olá, tudo bem? Que copa né?! Sermos batidos por 7×1, em casa, não é nada legal, mas poderia ser pior, em vez da Alemanha, a Argentina! Bom, independente do resultado da copa, esse assunto aqui pode realmente tirar seu sono. É comum entrar em discussões eternas sobre o erro de usar IoC e DI, e quero usar esse post para tonar isso mais óbvio.
Revisões
- Criação: Julho/2014
- Revisão: Abril/2018 – Adicionando exemplo HttpClientFactory após sua chegada.
- Revisão: Novembro/2023
- Migração de Editor clássico para Blocos do WordPress
- Nova capa (tamanho e estética)
- Revisão dos blocos de código para o novo Syntax Highlighter
IoC e DI
Vou usar os termos IoC e DI em todo o post, então vamos detalhar esses 2 acrônimos:
IoC: Inversion Of Control – Inversão de Controle. (Cuidado com a versão em PT-BR da Wikipedia, ela está errada!)
DI: Dependency Injection – Injeção de Dependência.
Um dos melhores posts para explicar IoC e DI está a aqui.
Problemas Comuns
Ao analisarmos projetos e diversos posts em diversos blogs, encontramos erros e mais erros na compreensão dos conceitos de IoC e DI. Embora sejam por uma questão de design inseparáveis, é possível ver IoC sem DI e DI sem IoC, e é aí que mora o problema.
Utilização de container IoC, sem fazer IoC e sem fazer DI
Abaixo mostro um exemplo:
public class HomeController { private readonly IExampleService _service; public HomeController() { _service = Container.Instance.Resolve<IExampleService>(); } public ActionResult Index() { return View(_service.GetSomething()); } }
Erros do exemplo:
1) Se uma classe requisita uma ou mais dependências ao container então não há IoC, muito menos DI. Neste caso o container IoC tem papel de Factory ou Service Locator, como preferir.
2) Resolver dependências baseadas exclusivamente em tipos gera erros de design. Ao escolher essa abordagem, está abdicando de:
- Ter em seu container, registros/configurações diferentes para uma mesma classe
- Ter em seu container, registros/configurações de uma segunda classe, que implemente o mesmo tipo.
Esses erros fazem com que você desenhe classes com maior acoplamento e acumulo responsabilidades. Você troca a possibilidade de ter uma implementação configurada diversas vezes, de forma diferente, para dar para a implementação a responsabilidade de compreender em qual contexto está inserido, e com isso, a demanda de tomar a decisão do que fazer.
Forma correta:
public class HomeController { private readonly IExampleService _service; public HomeController(IExampleService service) { _service = service; } public ActionResult Index() { return View(_service.GetSomething()); } }
As diferenças:
- Não é responsabilidade do HomeController saber que existe um container, sequer a dependência do container é necessária para esta classe.
- Embora o primeiro exemplo não explore a passagem de informações para a construção do objeto, esse seria um problema. Já no exemplo acima, não há essa relação entre HomeController e o Container.
Um ponto, que não abordei no exemplo mas vou abordar a seguir é a necessidade de mais de uma instância/implementação para um mesmo tipo(interface).
O problema do Register Resolve Release pattern
O problema não é o padrão em si, mas os exemplos proliferados na grande internet. Os exemplos conduzem a cometer o mesmo erro recorrente: Modelar errado!
Esse é o ponto que as pessoas geralmente não se atentam e quando explico: não entendem.
Imagine que você começou a criar um sistema e/ou serviço hoje em D0. Ao desenhar a infraestrutura de Logs, você desenhou uma interface ILogger semelhante à interface do exemplo 3:
public interface ILogger { void Log(string context, string content, LogLevel nivelLog, Dictionary<string, string> tags); }
A primeira implementação de ILogger que lhe vem à cabeça, dado seus requisitos, é o FileLogger. Passaram 2 dias, e no terceiro dia, D2, você criou 2 serviços, respectivamente Service1 e Service2, que dependem de uma implementação da interface ILogger para que possam gravar seus logs de operação. O exemplo 4, ajuda a elucidar.
public class FileLogger: ILogger { public void Log(string context, string content, LogLevel nivelLog, Dictionary<string, string> tags); { /* Grava o Log no Disco*/ } } public class Service1 { private ILogger _logger; public void Do() { this._logger.Log(...) } } public class Service2 { private ILogger _logger; public void Do() { this._logger.Log(...) } }
Agora, em D89, com 90 dias de desenvolvimento, seu sistema/serviço entrou em produção, e com mais 10 dias, houve a necessidade de segmentar os logs de Service1 e Service2. Independente da origem da necessidade, vou supor que essa exista. Agora você precisa que Service1 gere Logs no arquivo Service1.Log enquanto Service2 gere logs em Service2.log.
Vou dar alguns exemplos de solução:
- Mudar a interface ILogger para que o método Log receba também o parâmetro FileName, obviamente suas abstrações também receberiam.
- Mudar a interface ILogger para que o método Log receba também algum parâmetro de marcação que ajude as implementações de ILogger a descobrirem onde devem gravar o log (Enums, parâmetros de controle, etc)
- Implementar um ILogger diferente, que gerencie onde gravar os Logs e diga para o ILogger, mantendo a compatibilidade com os consumidores e executando os 2 passos descritos nos tópicos anteriores.
Se você considerou uma dessas 3 opções como a mais adequada para o cenário, tenho uma boa notícia para você: Este post é totalmente voltado para os erros que você comete no dia-a-dia. E sim, as 3 opções que dei estão completamente equivocadas!!! A má notícia, é que todas, ou pelo menos na maioria das vezes que optou por alguma dessas abordagens, você cometeu um sacrilégio.
Entenda:
Seu container provavelmente suporta a definição/registro de mais de uma implementações para um mesmo tipo (ILogger), podendo nomeá-las e configurá-las de forma independente. Provavelmente você não usa essa feature, pois 95% dos exemplos baseados em Register Resolve Release pattern, consistem em mostrar exatamente a abordagem simples, quando só há, somente, uma única implementação possível por tipo registrado.
Olhe esse exemplo de post do blog “Developer Tools Blogs” da própria Microsoft:
CodeLens for Git improvements in Visual Studio 2013 Ultimate Update 3 RC
Nesse caso, estamos falando do Roslyn, as implicações para este cenário é de que só pode existir no container uma única implementação de ISyntaxFactsService, bem como ISemanticFactsServices, e essa restrição limita suas possibilidades de modelagem.
Outro exemplo, está nas páginas do SimpleInjector:
https://simpleinjector.codeplex.com/
Interferência no Design
Se você considerou uma ou mais opções acima, como solução para o problema, você cometeu um erro de design. Me desculpe, não é algo subjetivo, ou com mérito de um “depende!”. O problema acontece, porque você não tomou uma decisão correta. Para este problema, sabemos que existe um erro comum, e diversas soluções corretas:
Aqui apresento uma das diversas soluções corretas, e a que considero a mais simples, que consiste em adicionar uma propriedade (estado) à classe FileLogger, como mostra o exemplo 5:
public class FileLogger: ILogger { protected string FileName {get; set;} public void Log(string context, string content, LogLevel nivelLog, Dictionary<string, string> tags); { /* Usa FileName para gravar o log em disco*/ } }
No container, ILogger seria registrado diversas vezes, tantas quantas seu FileLogger, precisar ser configurado diferente, como mostro no exemplo 6.
container.RegisterType<ILogger>("FileLoggerService1", () => new FileLogger(){ FileName = "Service1.log" }); container.RegisterType<ILogger>("FileLoggerService2", () => new FileLogger(){ FileName = "Service2.log" });
Onde está o erro?
As abordagens citadas como erradas, tendem a aumentar o acoplamento entre o consumidor e seu provedor.
Se optar pela adição do parâmetro FileName: A iteração passa a ser mais complexa, pois o consumidor precisa conhecer informações específicas da implementação do provedor, ignorando a abstração, e limitando-a.
Se optar pela utilização de parâmetros de marcação, a implementação do FileLogger teria sua complexidade aumentada, pois haveria necessidade de algum tipo de DE-PARA, ou em hard code ou usando configurações, discovery, etc.
As duas opções aumentam o acoplamento, e quanto maior o acoplamento, pior fica o design, menor é a testabilidade, maior é a complexidade.
Só para mostrar o tamanho do problema, imagine a necessidade de evolução que removesse o FileLogger, substituindo-o por um QueueLogger. Essa mudança não deveria afetar os consumidores de ILogger, mas dadas as características de complexidade adicionadas à interface, FileName deixaria de fazer sentido. Se FileName for uma propriedade, exclusiva da classe FileLogger, ótimo, nada precisa ser feito, mas se for um parâmetro, refatorar os consumidor é necessário.
Ainda há a terceira solução, onde induzia a criação de um gerenciador para que este, pudesse tomar a decisão, com base em alguma informação de contexto. Bom, nesse caso, uma classe a mais, não faz o menor sentido, dado os diversos padrões que podem ser aplicados para a solução.
Conclusões
Manter sua cabeça aberta, garantindo que a utilização ou não de containers não influencie seu design, é um bom ponto de partida. Sempre que você optar por configurar mais e programar menos, terá ganhos significativos. Não polua seu modelo com decisões que podem ser abstraídas por configurações, não deixe de criar várias implementações para uma determinada interface ou tipo base, se essa for sua real necessidade. Contornar esses problemas por causa do Container ou do exemplo padrão encontrado em um site, é um puta erro.
Grande abraço, volto em breve. Aliás, enquanto isso, há algumas leituras relevantes sobre esse assunto no tópico a seguir! Não perca!
Leituras recomendadas
Compose object graphs with confidence | http://blog.ploeh.dk/2011/03/04/Composeobjectgraphswithconfidence/
How not to do dependency injection – the static or singleton container | http://www.devtrends.co.uk/blog/how-not-to-do-dependency-injection-the-static-or-singleton-container
How not to do dependency injection – using xml over fluent configuration | http://www.devtrends.co.uk/blog/how-not-to-do-dependency-injection-using-xml-over-fluent-configuration
Constructor over-injection anti-pattern | http://jeffreypalermo.com/blog/constructor-over-injection-anti-pattern/
Review 24/04/2018
Estamos em 2018, acabou de sair o preview 2.1 do .NET Core. Uma das novidades nesse preview foi a chegada do HttpClientFactory, que traz consigo algo muito interessante. Primeiro, vamos à forma de utilização:
public void ConfigureServices(IServiceCollection services) { services.AddHttpClient("SomeCustomAPI", client => { client.BaseAddress = new Uri("https://someapiurl/"); client.DefaultRequestHeaders.Add("Accept", "application/json"); client.DefaultRequestHeaders.Add("User-Agent", "MyCustomUserAgent"); }); services.AddMvc(); }
Esse código foi tirado do post de HttpClientFactory for typed HttpClient instances in ASP.NET Core 2.1 direto do blog do Scott Hanselman .
Note que na linha 3 temos o argumento “SomeCustomAPI” sendo passado no método AddHttpClient. Esse é exatamente um dos problemas que aponto nesse post. Um problema decorrente de um erro fundamental adotado exclusivamente pela forma, amadora, com que o container de injeção de dependência do .NET Core foi projetado. Como disse no texto, onde mora o problema?
using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; namespace MyApp.Controllers { public class HomeController : Controller { private readonly IHttpClientFactory _httpClientFactory; public HomeController(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public Task<IActionResult> Index() { var client = _httpClientFactory.CreateClient("SomeCustomAPI"); return Ok(await client.GetStringAsync("/api")); } } }
Consegue ver que ao invés de já ter a dependência de um HttpClient, você passa a depender de um factory, e precisa passar uma string (que representa um nome na configuração). Uma classe a mais, uma inteligência a mais, um desvio do padrão. E tudo por causa da estratégia de Register/Resolver, que por sua vez é simplista demais para resolver um problema ligeiramente mais complexo do que o pattern se predispõem a resolver.
O problema é que na medida que não pode ter 2 implementações distintas da mesma interface direto no container, só lhe resta algumas gambiarras como:
- Fugir do reaproveitamento da interface
- Criar interfaces de marcação
- Fazer mapeamento pelo tipo concreto em vez da interface
- Adicionando classes de gestão de contexto, exatamente como vemos no caso do HttpClientFactory
A questão é que se você tem (ou deveria ter) 2 estratégias de Log, terá o mesmo problema, se você tiver 2 IDBCommand’s para mapear, também. Tudo que você não puder simplificar a uma única implementação para um determinado contrato demandará gambiarras como containers, wrappers, factories, dicionários, novas classes e/ou interfaces, até mesmo fazer com que quem consuma sua classe, dependa de mais uma abstração, desenhada exclusivamente para sanar um problema que era do container.
Como já disse aqui em outro post, uma gambiarra é igual a uma mentira: Você sempre precisará de mais para justificar a primeira.
0 comentários