Decorator Pattern usando o exemplo de um repositório em cache

Imagem de capa Decorator Pattern usando o exemplo de um repositório em cache

Decorator Pattern usando o exemplo de um repositório em cache

O padrão decorador permite adicionar (dinamicamente) comportamento a um objeto individual sem afetar o comportamento. Ele ajuda você com o princípio de responsabilidade única (consulte os princípios SOLID). Também adere à famosa citação: "Favor composição sobre herança". Portanto, é uma boa maneira de evitar subclasses e ainda adicionar funcionalidade estendida.

Normalmente você tem pelo menos 3 componentes: Uma interface, uma implementação concreta e o próprio decorador. A imagem a seguir mostrará como eles se correlacionam.

Tanto o decorador quanto a implementação concreta implementam o componente de interface. Mas o decorador também tem como campo a implementação concreta. O que isso permite é muito simples:

  • O mundo exterior não tem ideia de que somos um decorador, eles sempre verão um componente de interface
  • Temos pontos de extensão antes e depois da chamada de operação da implementação concreta

Repositório em cache

Inicialmente falei sobre a criação de um repositório em cache. Esta é apenas uma das muitas aplicações para esse padrão. Outro estaria registrando. Como podemos interceptar antes e depois da chamada "real", temos a capacidade de registrar, por exemplo, a solicitação e a resposta da nossa implementação concreta. De volta ao nosso repositório de cache.

A ideia é simples: temos uma interface IRepository que é nosso contrato com o mundo exterior. Precisamos de uma implementação fornecida pela classe SlowRepository e a decoradora Classe CachedRepository:

Por que SlowRepository? Você vai ver em um minuto. Primeiro a parte fácil: Nosso contrato IRepository:

public  interface  IRepository
{
   public Task<Person> GetPersonByIdAsync(int id);
   
   public Task SavePersonAsync(Person person);
}

Usaremos exatamente essa interface em nossa amostra em combinação com um DI-Container. Construímos um novo objeto Person e o colocamos no repositório. Depois carregamos desse exato repositório a pessoa com o Id 1.

var person = new Person(1, "Steven Giesel");
var repository = provider.GetRequiredService<IRepository>();
await repository.SavePersonAsync(person);

var stopwatch = Stopwatch.StartNew();
await repository.GetPersonByIdAsync(1);
Console.WriteLine($"First call took {stopwatch.ElapsedMilliseconds} ms");

stopwatch.Restart();
await repository.GetPersonByIdAsync(1);
Console.WriteLine($"Second call took {stopwatch.ElapsedMilliseconds} ms");

Agora, antes de podermos executar isso, precisamos de uma implementação real e agora você verá porque eu a chamei de SlowRepository:

public  class  SlowRepository : IRepository
{
    private  readonly List<Person> _people = new();

    public  async Task<Person> GetPersonByIdAsync(int id)
    {
        await Task.Delay(1000);
        return _people.Single(p => p.Id == id);
    }
   
    public Task SavePersonAsync(Person person)
    {
        _people.Add(person);
        return Task.CompletedTask;
    }    
}

Toda vez que chamamos GetPersonByIdAsync, esperamos 1 segundo antes de retornarmos o objeto. Claro que isso é para fins de demonstração. Agora vamos para a última parte: o próprio decorador. Vou mostrar o código primeiro e passar depois:

public  class  CachedRepository : IRepository
{
    private  readonly IMemoryCache _memoryCache; 
    private  readonly IRepository _repository;
   
    public  CachedRepository(IMemoryCache memoryCache, IRepository repository)
    {
        _memoryCache = memoryCache; 
        _repository = repository;
    }
   
    public async Task<Person> GetPersonByIdAsync(int id) 
    {
        if (!_memoryCache.TryGetValue(id, out Person value)) 
        {
             value = await _repository.GetPersonByIdAsync(id);
             _memoryCache.Set(id, value);
         }
        
         return  value;
     }
     
     public Task SavePersonAsync(Person person) 
     {
        return _repository.SavePersonAsync(person); 
      }
}
  • O CachedRepository também implementa o IRepository
  • Recebe IMemoryCache do DI-Container para fazer o cache para uso
  • Também recebemos um IRepository do DI-Container que usaremos posteriormente. Não sabemos se este é o SlowRepository
  • GetPersonByIdAsync verifica o IMemoryCache se ele tem um valor com o id fornecido. Caso contrário, acesse nosso IRepository e obtenha a pessoa via GetPersonByIdAsync. Armazene isso em nosso cache e retorne o valor. Se o cache tiver esse id, podemos retornar diretamente a pessoa do cache. Não há necessidade de ir ao repositório real!
  • SavePersonAsync apenas chama SavePersonAsync do repositório "real". Claro que poderíamos preencher o cache aqui também. Mas eu não queria ?? ... também por uma questão de demonstração.

Agora vamos recapitular aqui antes de continuarmos. Nosso decorador atua como um Proxy para o tipo real. Com isso podemos controlar o que acontece logo antes e depois da chamada real para o objeto. Em nosso caso CachedRepository, usamos o aspecto antes para verificar se um cache está ou não preenchido com o objeto solicitado. Se sim, retorne-o do cache, caso contrário, recupere da implementação real e preencha nosso cache.

Resultados

Vamos revisitar nosso trecho de cima. Qual você acha que é a saída no console?

var person = new Person(1, "Steven Giesel");
var repository = provider.GetRequiredService<IRepository>();
await repository.SavePersonAsync(person);

var stopwatch = Stopwatch.StartNew();
await repository.GetPersonByIdAsync(1);
Console.WriteLine($"First call took {stopwatch.ElapsedMilliseconds} ms");

stopwatch.Restart();
await repository.GetPersonByIdAsync(1);
Console.WriteLine($"Second call took {stopwatch.ElapsedMilliseconds} ms");

Impressões:

A primeira chamada demorou 1023 ms A segunda chamada demorou 0 ms

Então, o que acontece na primeira chamada:

  • Primeiro entramos em nosso CachedRepository e verificamos se o cache possui uma entrada com o Id 1
  • Como não possui nenhum objeto, ele chama o SlowRepository real para recuperar o item
  • Esta recuperação leva 1 segundo graças a Task.Delay(1000) em SlowRepository
  • Colocamos este item em nosso cache e o devolvemos ao chamador

O que acontece na segunda chamada:

  • Entramos novamente em nosso CachedRepository e verificamos se o cache possui uma entrada com o Id 1
  • Mas agora temos uma entrada
  • Portanto, não vamos para o SlowRepository e retornamos diretamente nosso objeto em cache

Essa é toda a magia que usamos. Nós dividimos claramente as responsabilidades e essa abordagem é super testável por unidade! Agora só falta uma parte. Conectando todas as coisas:

DI- Opcional

No meu exemplo anterior com o cronômetro mostrei que só uso IRepository. Além disso, meu CachedRepository usa apenas IRepository. Agora, quando você quiser usar isso com DI, precisará conectar todos os pontos por conta própria. Como usei um aplicativo de console, instalei esses dois pacotes: Microsoft.Extensions.DependencyInjection para o DI-Container. Este é o padrão quando você cria um novo projeto ASP.NET Core e Microsoft.Extensions.Caching.Memory para o MemoryCache.

Aqui o código para o DI-Container:

var provider = new ServiceCollection()
    .AddMemoryCache()
    .AddScoped<SlowRepository>()
    .AddScoped<IRepository>(p =>
   
    {
        var memoryCache = p.GetRequiredService<IMemoryCache>(); 
        var repository = p.GetRequiredService<SlowRepository>(); 
        return  new CachedRepository(memoryCache, repository);
    })
    .BuildServiceProvider()
  • AddMemoryCache adiciona o IMemoryCache ao contêiner
  • AddScoped< SlowRepository> adiciona o SlowRepository ao contêiner
  • O próximo bloco diz: "Ei, vamos registrar nosso CachedRepository como IRepository no contêiner. Eu tenho que resolver todas as dependências por uma razão simples. Meu CachedRepository quer um IRepository como segundo parâmetro. Mas não há nenhum objeto agora no gráfico de dependências que atende a isso, portanto, tenho que fazer isso manualmente.
  • É claro que registrar o SlowRepository no contêiner e colocá-lo em uma linha depois parece estranho e eu poderia usar return new CachedRepository(memoryCache, new SlowRepository()); mas lembre-se de que se o seu SlowRepository tiver uma nova dependência de construtor, você também precisará adotar esse código aqui. Não tão grande.

Recursos

Como sempre, você encontrará este exemplo no meu repositório do github, onde hospedo a maioria dos meus exemplos.

Epílogo

Agora é bastante discutível se o exemplo mostrado é o padrão decorador ou padrão proxy. Do ponto de vista estrutural, ambos são muito, muito semelhantes. Depende da sua definição daqueles onde você classificaria o exemplo dado ??