Entendendo a diferença entre Injeção de Dependência e Inversão de Dependência em .NET 6

Na programação orientada a objetos, dois conceitos fundamentais são frequentemente mencionados: Injeção de Dependência (DI — Dependency Injection) e Inversão de Dependência (IoC — Inversion of Control). Ambos são essenciais no desenvolvimento de software moderno, especialmente ao lidar com a manutenção, testabilidade e escalabilidade do código. No contexto do .NET 6, vamos mergulhar mais fundo nesses conceitos e entender como eles são aplicados.

Inversão de Dependência (IoC):

A Inversão de Dependência é um princípio que visa inverter o fluxo de controle no seu código. Em vez de um componente de alto nível depender diretamente de componentes de baixo nível, a IoC propõe que as dependências sejam gerenciadas por um componente externo. Em outras palavras, as classes de nível superior não devem criar ou gerenciar suas próprias dependências, mas sim recebê-las de uma fonte externa.

Um exemplo de implementação disso em .NET 6 seria usar interfaces para definir contratos e, em seguida, usar essas interfaces em vez de classes concretas. Aqui está um exemplo simples:

// Interface que define um serviço
public interface IExemploServico
{
    void RealizarAcao();
}

// Implementação do serviço
public class ExemploServico : IExemploServico
{
    public void RealizarAcao()
    {
        Console.WriteLine("Ação realizada pelo serviço exemplo.");
    }
}

// Classe que depende do serviço usando IoC
public class ClasseQueUsaServico
{
    private readonly IExemploServico _servico;
    // Perceba que aqui eu não estou instanciando a classe em um novo 
    //objeto usando a palavra chave new e sim injetando a inferface extendida
    //pela classe. Ex. MinhaClasse : IMinhaClasse
    public ClasseQueUsaServico(IExemploServico servico)
    {
        _servico = servico;
    }

    public void ExecutarAcaoDoServico()
    {
        _servico.RealizarAcao();
    }
}

Em .Net é importante entender onde essas classes são instanciadas, como são gerenciados os ciclos de vida dessas instancias e como elas são despejadas (não sei se o termo certo seria esse). No exemplo acima para usar a interface IExemploServico de forma a ser injetado na classe ao qual ela será usada. Normalmente adicionadas na class Startup ou na classe Program nas versões mais novas. Na classe Program de uma api .Net6 por exemplo o escopo das instâncias das classes referenciadas pelas interfaces na injeção de decência definem como serão usadas em relação ao gerenciamento do uso dessa interface na aplicação, por exemplo.

services.AddSingleton<IExemploServico, ExemploServico>();
services.AddTransient<IExemploServico, ExemploServico();
services.AddScoped<IExemploServico, ExemploServico>();
  • Singleton: é criada uma única instância para todas requisições. Em outras palavras, é criada uma instância a primeira vez que é solicitada e todas as vezes seguintes a mesma instância é usada (design pattern singleton);
  • Scoped: é criada uma única instância por requisição. Ou seja, usando como exemplo de uma aplicação Web, quando se recebe uma nova requisição, por exemplo um click num botão do outro lado do navegador, é criada uma instância, onde o escopo é essa requisição. Então se for necessária a dependência (serviço registrado) múltiplas vezes na mesma requisição a mesma instância será usada. Seria como um “Singleton para uma requisição”;
  • Transient: sempre é criada uma nova instância cada vez que for necessário, independente da requisição, basicamente um new XXX cada vez que for necessário usar a dependência.

Um pequeno quadro para ilustrar:

O impacto no ciclo de vida é o que foi comentado acima, ou seja, com exceção do Singleton, o Scoped e Transient são impactados pelo número de requisições.

Aqui vale notar que, num serviço sem estado (stateless) ou uma aplicação sem contexto de requisição, como um “Console” por exemplo, Scoped pode ter o mesmo comportamento de Transient, uma vez que se não for possível validar se está numa mesma requisição, sempre uma nova instância será criada.

public class Program{
  public void Main()
  {
   var init = new StartUp();
   var applicationService = init.Services.GetService<IApplicationService>();
   
   applicationService.FirstMethod();
   applicationService.SecondMethod();
  }
}


public class StartUp
{
 private ServiceCollection _services;
 
 public StartUp()
 {
  _services = new ServiceCollection();
  RegisterServices(_services);
  
  var serviceProvider = _services.BuildServiceProvider();
  var service = serviceProvider.GetService<IMyClass>();
 }
 
 private void RegisterServices(IServiceCollection services)
 {
  services.AddTransient<IMyClass2, MyClass2>();
  services.AddScoped<IMyClass, MyClass>();
  
  services.AddScoped<IApplicationService, ApplicationService>();
 }
 
 public ServiceProvider Services
 { 
  get { 
  return _services.BuildServiceProvider();
 }
}

public class ApplicationService : IApplicationService
{
 private IMyClass _myClass;
 private IMyClass2 _myClass2;
 
 public ApplicationService(IMyClass myClass, IMyClass2 myClass2)
 {
  _myClass = myClass;
  _myClass2 = myClass2;
 }
 
 public void FirstMethod()
 {
  Console.WriteLine(_myClass.GetNome());
 }
 
 public async Task SecondMethod()
 {
  var result = await Task.FromResult(_myClass2.GetNome());
  Console.WriteLine(result);
 }
}


public interface IMyClass
{
 string GetNome();
}

public interface IMyClass2
{ 
 string GetNome();
}

public interface IApplicationService
{
 void FirstMethod();
 Task SecondMethod();
}

public class MyClass : IMyClass
{
 public string GetNome()
 {
  return "John Doe";
 }
}

//Os objetos serão limpos quando não estiverem mais em uso e quando o garbage collector achar conveniente.
public class MyClass2 : IMyClass2, IDisposable
{
 public string GetNome()
 {
  return "Jonh Doe";
 }
 
 public void Dispose()
    {
        Dispose();
        GC.SuppressFinalize(this);
    }
}

Nesse exemplo foram criados duas classes simples com um único método, elas poderia representar os serviços chamados por uma api e seus ciclos de vida seriam tratados de forma diferente, no services.scoped<IMyClass, MyClass> sempre que houver a necessidade de se utilizar uma instancia dessa classe ela será injetada na classe, usada e despejada em seguida, no caso da classe definida como service.transiente<IMyClass2, MyClass> ela seria injetada no construtor da classe, nesse exemplo a ApplicationService e toda vez que fosse feito uma solicitação uma nova instância seria criada e senão despejada ocupará o espaço na memória dessa nova instância, na opção service.Singleton<IMyClass2, MyClass> uma instância será criada no inicio da execução da aplicação e seu ciclo de vida se manterá até que a aplicação deixe de ser executada.