Inversion of Control

Inversion of Control, ou IoC, é um princípio de design que busca melhor plugabilidade, testabilidade, usabilidade e baixo acoplamento entre as partes de um software. Em português, o termo aparece como inversão de controle, e ele está por trás de boa parte do que fazemos no .NET sem perceber.

Neste artigo, você vai entender a origem do conceito, a relação com Dependency Injection e com o Dependency Inversion Principle. Depois, vai ver um exemplo prático que sai de um controller acoplado até um código desacoplado com interfaces.

O que é Inversion of Control

Inversion of Control é um princípio de software que inverte o fluxo de controle em comparação com abordagens tradicionais, como a programação procedural. Na programação procedural, o seu código decide tudo: o que chamar, quando chamar e em que ordem.

No IoC, os pedaços de software que criamos recebem o fluxo, e não o contrário. Ou seja, enquanto a programação procedural expressa diretamente o que precisa ser feito, no IoC nós delegamos parte desse controle para bibliotecas, frameworks ou contêineres externos.

Na prática, o código customizado passa a preencher lacunas. Quem decide o momento de executar cada parte é um componente externo, normalmente um framework.

Princípio Hollywoodiano

Esse conceito também é conhecido como Princípio Hollywoodiano, que diz o seguinte: "Não nos chame, nós chamamos você".

A brincadeira reflete diretamente o que queremos no código: pequenos pedaços que agem de forma independente e servem em diversas partes do sistema. Esses pedaços não comandam o fluxo da aplicação, pois quem os aciona é o framework, no momento em que precisa deles.

Pense em um teste de elenco em Hollywood. O ator deixa o contato, e a produtora liga quando houver um papel, então o ator não fica telefonando todos os dias.

Da mesma forma, o seu componente apenas declara o que sabe fazer. Em seguida, a infraestrutura decide quando executá-lo.

História da Inversion of Control

A Inversion of Control serve para aumentar a modularidade dos sistemas, criar extensibilidade e aproveitar melhor a OOP (Programação Orientada a Objetos). Suas raízes estão nos estudos sobre frameworks orientados a objetos do fim dos anos 1980, como o trabalho de Ralph Johnson e Brian Foote em 1988.

O termo foi usado por Michael Mattsson em uma tese de 1996 sobre frameworks orientados a objetos. Depois, o projeto Apache Avalon ampliou o uso da expressão no fim dos anos 1990.

Contudo, a popularidade veio por volta de 2004, quando Martin Fowler e Robert C. Martin (Uncle Bob) passaram a citar e discutir o conceito. Foi nessa época que o nome Dependency Injection surgiu para descrever uma forma específica de IoC.

Framework versus biblioteca

A diferença entre framework e biblioteca é a forma mais simples de enxergar a inversão. Quando você usa uma biblioteca, o seu código chama a biblioteca e mantém o controle.

Por outro lado, quando você usa um framework, é o framework que chama o seu código. Você escreve as partes específicas, e ele orquestra a execução.

O ASP.NET Core é um ótimo exemplo. Você nunca chama o método de um controller diretamente, porque o pipeline recebe a requisição HTTP, escolhe a rota, cria o controller e invoca a action.

O mesmo acontece com um serviço em segundo plano. Você sobrescreve um método, e o host decide quando executá-lo:

public class ReportWorker : BackgroundService
{
    // O host chama este método; nosso código apenas preenche a lacuna
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            Console.WriteLine("Generating report...");
            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }
}

Esse padrão se chama Template Method, e é uma das técnicas clássicas de Inversion of Control. Além dele, callbacks, eventos, Strategy e Service Locator também invertem o fluxo.

Callbacks e eventos como inversão de controle

Callbacks e eventos também são formas de inverter o controle. Nesse caso, você entrega uma função para outro componente, e ele decide quando chamá-la.

O método List<T>.Sort mostra isso de forma simples. Você informa apenas a regra de comparação, enquanto o algoritmo de ordenação define quantas vezes e em que ordem vai usá-la:

var customers = new List<Customer>
{
    new("Maria", 32),
    new("John", 25)
};

// Passamos a regra; o Sort decide quando executá-la
customers.Sort((left, right) => left.Age.CompareTo(right.Age));

public record Customer(string Name, int Age);

Os eventos seguem a mesma ideia. Um botão em uma aplicação desktop dispara o evento Click, e o seu handler roda apenas quando o usuário clica.

Repare que, em todos esses casos, o seu código não controla o momento da execução. Essa é a essência da inversão de controle.

Inversion of Control e Dependency Injection

Embora esteja relacionada com DI (Dependency Injection, ou Injeção de Dependência) e DIP (Dependency Inversion Principle, ou Princípio da Inversão de Dependência), a Inversion of Control se difere de ambos.

DI é a técnica que utilizamos para alcançar o IoC no tratamento de dependências. Em vez de a classe criar os próprios colaboradores, alguém de fora os entrega prontos.

A documentação oficial do .NET descreve exatamente isso. Segundo a documentação de injeção de dependência do .NET, DI é uma técnica para alcançar Inversion of Control entre as classes e suas dependências.

Já o DIP provê um maior desacoplamento entre as camadas. Ele orienta o código a depender de abstrações, e não de implementações.

Se quiser se aprofundar nos dois, leia os artigos sobre Dependency Injection e sobre o Dependency Inversion Principle. Eles complementam este conteúdo.

Inversion of Control na prática

Para entender melhor esse princípio, vamos tomar como base o código abaixo. Ele precisa recuperar o cliente do banco e faz essa chamada de forma direta, dentro da action.

public class OrderController : Controller
{
    [Route("v1/orders")]
    [HttpPost]
    public async Task<string> Place(PlaceOrderCommand command)
    {
        // #1 - Recupera o cliente
        Customer? customer = null;
        using (var connection = new SqlConnection("CONN_STRING"))
        {
            const string query = "SELECT * FROM [Customer] WHERE [Id]=@id";
            customer = await connection.QueryFirstOrDefaultAsync<Customer>(
                query,
                new { id = command.CustomerId });
        }

        // ... restante do processamento do pedido
        return "Order placed";
    }
}
Banner do curso Dominando Injeção de Dependência do balta.io, que aprofunda Inversion of Control e DI no .NET
O curso Dominando Injeção de Dependência aprofunda os conceitos deste artigo.

Quer ir além da teoria? O curso Dominando Injeção de Dependência aborda esses conceitos em detalhes.

Os problemas do código acoplado

Se notarmos, esse código está acoplado. Isso significa que não conseguimos reutilizar a pesquisa por cliente em nenhum outro ponto do sistema.

A manutenção também fica difícil, pois precisamos lembrar de alterar todo lugar que repete esse código. Além disso, a legibilidade é ruim, já que existe muita coisa para se preocupar acontecendo no mesmo método.

Há ainda o problema dos testes. Para testar a action, você precisa de um banco de dados real, porque a classe cria a conexão por conta própria.

A Microsoft resume esse cenário com a expressão "new is glue". Cada new de uma classe de infraestrutura cola o seu código a uma implementação específica.

Extraindo a responsabilidade

A solução é externalizar esse código e chamá-lo apenas quando precisamos. Dessa forma, temos um pedaço pequeno, reusável, fácil de ler, de testar e de manter.

public class CustomerRepository
{
    public async Task<Customer?> GetByIdAsync(Guid id)
    {
        // A conexão fica isolada dentro do repositório
        using var connection = new SqlConnection("CONN_STRING");
        const string query = "SELECT * FROM [Customer] WHERE [Id]=@id";
        return await connection.QueryFirstOrDefaultAsync<Customer>(query, new { id });
    }
}

Agora a consulta mora em um único lugar. Se a tabela mudar, você altera apenas o repositório.

Ainda assim, o controller continuaria criando o repositório com new. Por isso, o próximo passo é aplicar a Inversion of Control na forma de injeção de dependência.

Aplicando Inversion of Control com Dependency Injection

Com o código separado, podemos usar Dependency Injection para desacoplar o controller. Ele deixa de criar o repositório e passa a recebê-lo pelo construtor.

public class OrderController : Controller
{
    private readonly CustomerRepository _repository;

    public OrderController(CustomerRepository repository)
        => _repository = repository;

    [Route("v1/orders")]
    [HttpPost]
    public async Task<string> Place(PlaceOrderCommand command)
    {
        // #1 - Recupera o cliente
        var customer = await _repository.GetByIdAsync(command.CustomerId);

        // ... restante do processamento do pedido
        return "Order placed";
    }
}

Note que agora temos uma dependência do CustomerRepository. Em outras palavras, não conseguimos criar uma instância do OrderController sem informar um CustomerRepository.

Essa é a inversão em ação. O controller não controla mais a criação do colaborador, pois quem fornece o objeto é o framework.

O construtor também deixa as necessidades da classe explícitas. A documentação de arquitetura do .NET chama isso de Explicit Dependencies Principle.

Dependency Injection no .NET

Como resultado, precisamos resolver essas dependências para executar o código. O .NET traz um contêiner de injeção de dependência embutido, e muita gente o compara a um Service Locator, porque ele localiza e entrega as instâncias registradas.

O registro acontece no Program.cs, por meio do builder.Services:

builder.Services.AddTransient<CustomerRepository>();

Quando o ASP.NET Core cria o OrderController, ele consulta o contêiner. Então, o contêiner cria o CustomerRepository e o entrega no construtor.

Vale lembrar que o .NET possui vários métodos para gerenciar o ciclo de vida dos objetos, como AddScoped e AddSingleton. A tabela abaixo resume cada um deles:

Método Ciclo de vida
AddTransient Cria uma nova instância sempre que alguém solicita o serviço
AddScoped Cria uma instância por escopo, normalmente uma por requisição HTTP
AddSingleton Cria uma única instância para toda a vida da aplicação

Escolher o ciclo de vida certo evita bugs sutis. Por exemplo, a documentação alerta para não resolver um serviço scoped dentro de um singleton, porque ele passaria a se comportar como singleton.

Se quiser ver o contêiner do ASP.NET Core em mais detalhes, o artigo sobre injeção de dependência no ASP.NET Core aprofunda o tema.

Dependency Inversion junto com Inversion of Control

Nesse cenário, o DIP cria um desacoplamento ainda maior entre o controller e o repositório. Para isso, ele introduz interfaces, ou seja, abstrações.

public interface ICustomerRepository
{
    Task<Customer?> GetByIdAsync(Guid id);
}

public class CustomerRepository : ICustomerRepository
{
    // Mesma implementação de antes, agora atrás de uma interface
    public async Task<Customer?> GetByIdAsync(Guid id)
    {
        using var connection = new SqlConnection("CONN_STRING");
        const string query = "SELECT * FROM [Customer] WHERE [Id]=@id";
        return await connection.QueryFirstOrDefaultAsync<Customer>(query, new { id });
    }
}

O controller passa a depender apenas da interface:

public class OrderController : Controller
{
    private readonly ICustomerRepository _repository;

    public OrderController(ICustomerRepository repository)
        => _repository = repository;

    [Route("v1/orders")]
    [HttpPost]
    public async Task<string> Place(PlaceOrderCommand command)
    {
        // #1 - Recupera o cliente
        var customer = await _repository.GetByIdAsync(command.CustomerId);

        // ... restante do processamento do pedido
        return "Order placed";
    }
}

Por fim, o registro informa ao contêiner qual implementação entregar para a interface:

builder.Services.AddTransient<ICustomerRepository, CustomerRepository>();

Agora você pode trocar o CustomerRepository por outra implementação sem tocar no controller. Por exemplo, um repositório em memória para testes ou uma versão que usa cache.

Testando código com Inversion of Control

O maior ganho do desacoplamento aparece nos testes. Como o controller depende de ICustomerRepository, basta entregar uma implementação falsa para validar o comportamento.

public class FakeCustomerRepository : ICustomerRepository
{
    // Retorna sempre o mesmo cliente, sem acessar o banco
    public Task<Customer?> GetByIdAsync(Guid id)
        => Task.FromResult<Customer?>(new Customer { Id = id, Name = "Test customer" });
}

public class OrderControllerTests
{
    [Fact]
    public async Task Place_ShouldReturnSuccess_WhenCustomerExists()
    {
        var controller = new OrderController(new FakeCustomerRepository());

        var result = await controller.Place(new PlaceOrderCommand { CustomerId = Guid.NewGuid() });

        Assert.Equal("Order placed", result);
    }
}

Esse teste roda em milissegundos e não precisa de connection string. Além disso, ele falha apenas quando a regra do controller muda, e não quando o banco está indisponível.

Na primeira versão do código, esse teste seria impossível sem um banco real. Portanto, a Inversion of Control transforma diretamente a forma como você valida o sistema.

Benefícios da Inversion of Control

O primeiro benefício é a testabilidade. Com a interface, um teste unitário entrega um repositório falso ao controller e valida a regra sem banco de dados.

O segundo é a plugabilidade. Assim como no exemplo do cache, você troca implementações apenas mudando o registro no contêiner.

O terceiro é a manutenção. Cada classe tem uma responsabilidade, e as mudanças ficam concentradas em um único ponto.

Esses ganhos conversam diretamente com os princípios SOLID. Afinal, o DIP é a letra D do acrônimo, e a Inversion of Control é o mecanismo que o torna prático.

Inversion of Control nos middlewares do ASP.NET Core

Os middlewares do ASP.NET Core são outro exemplo claro de Inversion of Control. Você registra um delegate no pipeline, e o servidor o executa a cada requisição.

var app = builder.Build();

// O pipeline chama este delegate para cada requisição recebida
app.Use(async (context, next) =>
{
    var startedAt = DateTime.UtcNow;
    await next(context);
    var elapsed = DateTime.UtcNow - startedAt;
    Console.WriteLine($"{context.Request.Path} took {elapsed.TotalMilliseconds} ms");
});

app.MapControllers();
app.Run();

Nesse trecho, o seu código não sabe quando uma requisição vai chegar. Ele apenas declara o que fazer, e o framework decide o momento.

Além disso, o próprio next é uma inversão. O middleware recebe o próximo passo do pipeline pronto, sem precisar saber quem vem depois.

Esse desenho permite que autenticação, logs, compressão e roteamento convivam no mesmo pipeline. Cada peça fica isolada e substituível, exatamente como prega o princípio.

Cuidados ao aplicar IoC

Inversão de controle não é sinônimo de criar interface para tudo. Classes sem dependências externas, como um record de valor, podem continuar sendo criadas com new sem prejuízo.

Também vale observar o tamanho dos construtores. Se uma classe recebe dependências demais, isso normalmente indica que ela faz coisas demais e viola o princípio da responsabilidade única.

Por fim, evite usar o contêiner como Service Locator dentro das classes. Pedir serviços ao IServiceProvider no meio do código esconde as dependências e anula parte dos benefícios.

Conclusão

IoC, DI e DIP são termos que aparecem juntos com frequência, mas têm finalidades diferentes. A Inversion of Control é o princípio que tira o controle do fluxo do seu código e o entrega a um componente externo.

Enquanto a DI é a técnica que aplica esse princípio às dependências, o DIP orienta o código a depender de abstrações. Juntos, os três buscam o mesmo objetivo: baixo acoplamento.

No .NET, você já usa Inversion of Control o tempo todo, seja no pipeline do ASP.NET Core, seja no contêiner de serviços. Entender o conceito ajuda a escrever código mais simples de testar, manter e evoluir.

Por isso, comece pelo básico: extraia responsabilidades, receba dependências pelo construtor e registre tudo no contêiner. Com o tempo, a Inversion of Control vira um hábito natural no seu dia a dia.

FAQ

Inversion of Control e Dependency Injection são a mesma coisa?

Não. IoC é o princípio geral de inverter o fluxo de controle, enquanto DI é uma técnica específica que aplica esse princípio à criação e entrega de dependências.

O que é o Princípio Hollywoodiano?

É o apelido da inversão de controle, resumido na frase "Não nos chame, nós chamamos você". O framework chama o seu código quando precisa dele.

O .NET tem um contêiner de IoC nativo?

Sim. O pacote Microsoft.Extensions.DependencyInjection oferece um contêiner embutido, usado pelo ASP.NET Core, pelos Worker Services e por outros tipos de aplicação.

Quando usar AddTransient, AddScoped ou AddSingleton?

Use transient para serviços leves e sem estado, scoped para serviços ligados à requisição, como um DbContext, e singleton para objetos compartilhados por toda a aplicação.

Preciso criar uma interface para toda classe?

Não. Interfaces fazem sentido quando há variação de implementação ou necessidade de testes isolados, e classes simples sem dependências externas podem continuar concretas.