Conheça os Keyed Services do .NET 8 e como te ajudam a implementar o Open Closed Principle do SOLID

Olá! Tudo certo?

Com a chegada do .NET 8, diversos recursos foram adicionados ou melhorados. Uma das novidades é a possibilidade de utilizar injeção de dependência com “chaves”: os Keyed Services. Neste post vamos conhecer um pouco mais sobre isso!

Caso você ainda não conheça sobre injeção de dependência, pode ler sobre aqui.

Como era antes do .NET 8?

Até então, para utilizarmos a injeção de dependência no .NET, poderíamos usar, principalmente mas não somente, os métodos AddSingleton, AddScoped e AddTransient.

Como exemplo, para uma classe Mensagem, que herda da interface IMensagem, como escrito abaixo:

public interface IMensagem
{
string RetornaMensagem(string nome);
}
public class MensagemBoasVindas : IMensagem
{
public string RetornaMensagem(string nome)
{
return $"Olá {nome}, estamos felizes em lhe receber!";
}
}
view raw Mensagem.cs hosted with ❤ by GitHub

Podíamos realizar a injeção de dependência da seguinte forma:

var builder = WebApplication.CreateBuilder(args);
//…outras configurações
builder.Services.AddScoped<IMensagem, Mensagem>();
//Outras configurações…
view raw Program.cs hosted with ❤ by GitHub

Mas o que é Open Closed Principle (OCP) mesmo?

Brevemente, o princípio SOLID conhecido como Open Closed, definido por Uncle Bob, destaca a capacidade de estender o comportamento de uma classe sem modificá-la. Em outras palavras, as classes devem estar sempre abertas para extensão, porém fechadas para alterações. Esse princípio ajuda a evitar bugs e comportamentos indesejados ao adicionar um novo comportamento. Para mais detalhes, consulte este artigo de Uncle Bob.

Para entendermos melhor na prática, imagine a seguinte situação: Temos uma lista de clientes que devemos enviar uma mensagem de boas-vindas. Atualmente temos dois tipos de clientes: padrão e premium. E cada tipo de cliente tem uma mensagem diferente, por exemplo:

  • Padrão: “Olá, [NOME], estamos felizes que esteja conosco!”
  • Premium: “Olá, [NOME], estamos felizes que esteja conosco! Você tem 50% de desconto para a primeira compra”

Na abordagem abaixo, sem a utilização de OCP, perceba que para cada novo tipo de cliente, deve ser adicionado uma nova estrutura condicional:

public class MensagemBoasVindas
{
public string CriarMensagem(Cliente cliente)
{
if (cliente.Tipo == "Padrão")
{
return $"Olá, {cliente.Nome}, estamos felizes que esteja conosco!";
}
else if (cliente.Tipo == "Premium")
{
return $"Olá, {cliente.Nome}, estamos felizes que esteja conosco! Você tem 50% de desconto para a primeira compra.";
}
else
{
throw new NotImplementedException();
}
}
}
view raw SemSolid.cs hosted with ❤ by GitHub

Já, nesta abordagem, utilizando OCP, para cada novo tipo de cliente, a classe de mensagem é estendida, porém nunca alterada:

public interface IMensagemBoasVindas
{
string CriarMensagem(Cliente cliente);
}
public class MensagemBoasVindasPadrao : IMensagemBoasVindas
{
public string CriarMensagem(Cliente cliente)
{
return $"Olá, {cliente.Nome}, estamos felizes que esteja conosco!";
}
}
public class MensagemBoasVindasPremium : IMensagemBoasVindas
{
public string CriarMensagem(Cliente cliente)
{
return $"Olá, {cliente.Nome}, estamos felizes que esteja conosco! Você tem 50% de desconto para a primeira compra.";
}
}

Com isso, centralizamos a dependência na interface IMensagemBoasVindas e para cada tipo de cliente diferente podemos instanciar a classe correta.

E como a implementação ficaria com a Injeção de Dependência?

Quando uma interface é registrada pra mais de uma classe utilizando o método AddScoped como no exemplo abaixo:

builder.Services.AddScoped<IMensagemBoasVindas, MensagemBoasVindasPadrao>();
builder.Services.AddScoped<IMensagemBoasVindas, MensagemBoasVindasPremium>();
view raw AddScoped.cs hosted with ❤ by GitHub

Podemos recuperar as instâncias de duas maneiras: Apenas uma instância, onde a classe recuperada é a última que foi registrada, MensagemBoasVindasPremium, no nosso caso. E também podemos recuperar todas as instâncias registradas para aquela interface, recebendo uma coleção no construtor. Abaixo estão ambas as maneiras:

Recebendo apenas uma instância:

public class ClienteController : ControllerBase
{
private readonly IMensagemBoasVindas _mensagemBoasVindas;
public ClienteController(IMensagemBoasVindas mensagemBoasVindas)
{
_mensagemBoasVindas = mensagemBoasVindas;
}
//demais métodos…
}
view raw Construtor.cs hosted with ❤ by GitHub

Ao rodar este código, o objeto instanciado na variável mensagemBoasVindas é o MensagemBoasVindasPremium, pois foi vinculado por último, veja:

Recebendo todas as instâncias:

public class ClienteController : ControllerBase
{
private readonly IEnumerable<IMensagemBoasVindas> _mensagensBoasVindas;
public ClienteController(IEnumerable<IMensagemBoasVindas> mensagensBoasVindas)
{
_mensagensBoasVindas = mensagensBoasVindas;
}
//demais métodos…
}

Ao rodar este código, a variável mensagensBoasVindas contém uma lista com todas as implementações possíveis, veja:

O “problema” aqui é que como a injeção de dependência não sabe qual a instância adequada para cada caso, para obtermos a instância correta precisaríamos adicionar um atributo “Tipo” em cada classe específica, buscar todas as implementações e depois filtrar este atributo para obter a instância correta, é um pequeno “ajuste” para conseguirmos a instância correta sem fazer vários IFs ou até criar uma interface diferente, com os mesmos métodos e atributos que a interface IMensagemBoasVindas para obter de maneira adequada no construtor.

Como podemos fazer utilizando Keyed Services?

Com a chegada dos Keyed Services, podemos dar um “apelido”, ou melhor, uma chave para cada instância registrada a uma interface por meio dos métodos AddKeyedScoped, AddKeyedSingleton ou AddKeyedTransient, veja:

builder.Services.AddKeyedScoped<IMensagemBoasVindas, MensagemBoasVindasPadrao>("Padrao");
builder.Services.AddKeyedScoped<IMensagemBoasVindas, MensagemBoasVindasPremium>("Premium");
view raw Program.cs hosted with ❤ by GitHub

Com isso, é possível obter a instância atrelada a uma interface com uma chave específica, como no exemplo abaixo:

public class ClienteController : ControllerBase
{
private readonly IMensagemBoasVindas _mensagemBoasVindas;
public ClienteController([FromKeyedServices("Padrao")] IMensagemBoasVindas mensagemBoasVindas)
{
_mensagemBoasVindas = mensagemBoasVindas;
}
//demais métodos…
}

Podemos verificar que a instância na variável mensagemBoasVindas é da classe MensagemBoasVindasPadrao, por conta da chave “Padrao” utilizada.

Porém, no nosso caso, a classe que devemos instanciar é referente ao atributo TipoCliente da classe Cliente. O que podemos fazer, é recuperar a instância utilizando este atributo, da seguinte maneira:

public IActionResult EnviarMensagem(IServiceProvider serviceProvider, int clienteId)
{
//busca o cliente pelo clienteId informado
var cliente = new Cliente
{
Nome = "Vinicius Mussak",
TipoCliente = "Premium",
Id = clienteId
};
//Recupera a instância pelo tipo do cliente
var service = serviceProvider.GetRequiredKeyedService<IMensagemBoasVindas>(cliente.TipoCliente);
var mensagem = service.CriarMensagem(cliente);
return Ok(mensagem);
}
view raw ActionKey.cs hosted with ❤ by GitHub

Ao acionarmos o método EnviarMensagem, e obtermos a instância de IMensagemBoasVindas utilizando o TipoCliente, que neste caso é “Premium”, obtemos a instância da classe MensagemBoasVindasPremium como podemos ver abaixo:

Finalizando

O novo recurso de Keyed Services pode nos ajudar a economizar código e reduzir a necessidade de interfaces idênticas que são utilizadas exclusivamente para injeção de dependência.

Os códigos utilizados estão disponíveis no GitHub: https://github.com/vmussak/keyed-services-exemplo

Por hoje é só isso, qualquer dúvida ou sugestão, estou à disposição! Até mais 😀

2 thoughts on “Conheça os Keyed Services do .NET 8 e como te ajudam a implementar o Open Closed Principle do SOLID

  1. Oi, Vinícius, espero que esteja tudo beleza contigo.

    Procurei outras formas de contatá-lo para tirar uma dúvida, mas não encontrei então quero pedir desculpas por não estar comentando sobre a postagem.

    Agora eu gostaria de agradecer e parabenizá-lo pelo curso de SOLID e Design Patterns em C# oferecido em parceria com a ada.tech. Estou gostando bastante das suas explicações, exemplos e didática.

    Durante o curso, tive uma pequena dúvida em relação a aplicação do princípio de Segregação de Interface, pois em um trecho do código uma classe externa à hierarquia de classes de Item precisa verificar o tipo da instância para identificar se deve ou não calcular a taxa. Não tenho certeza, mas acho que a classe pedido acaba sendo responsável por isso.

    Minha dúvida é: esse tipo de checagem do tipo da instância em uma classe externa seria a violação de algum princípio?

    Seria possível contornar essa checagem com um hook method?

    Gostar

    • viniciusmussak's avatar viniciusmussak 24/05/2024 / 15:30

      Fala Edu! Tudo certo?

      Fico feliz que tenha curtido!

      Fique a vontade para entrar em contato pelo linkedin “Vinicius Mussak” ou pelo email “vincius.mussak(at)ada(dot)tech”

      Seria possível sim adicionarmos um hook na classe item para validar se possui calculo de taxas, removendo a responsabilidade do “Pedido” sobre isso. É um ótimo ponto que levantou inclusive!

      Fico à disposição caso queira falar mais sobre este e outros assuntos!

      Abraço!

      Liked by 1 person

Deixe um comentário