Padrão de decorador C#

Resumo : neste tutorial, você aprenderá como estender o comportamento de um objeto dinamicamente sem usar herança.

Introdução ao padrão C# Decorator

O padrão Decorator é um padrão estrutural que permite estender ou modificar o comportamento de um objeto sem alterar a implementação original do objeto.

O diagrama UML a seguir ilustra o padrão Decorator:

O padrão Decorator consiste nos seguintes elementos:

  • Component: esta é a interface que define as operações que um objeto pode realizar. Pode Componentser uma interface ou uma classe abstrata . O Componentdefine um objeto que será decorado.
  • ConcreteComponent: Esta é a classe que implementa a Componentinterface.
  • Decorator: esta é uma classe abstrata que implementa a Componentinterface e contém uma referência ao Componentobjeto.
  • ConcreteDecorator: esta é a classe que estende a Decoratorclasse e adiciona comportamento adicional ao Componentobjeto.

Neste diagrama, a Decoratorclasse herda do Component. Mas ele usa herança apenas para obter correspondência de tipo , não para reutilizar a funcionalidade do Component.

Exemplo de padrão Decorador C#

Vamos dar um exemplo para entender como funciona o padrão Decorator.

Desenvolvendo um programa que chama uma API

Suponha que você precise desenvolver um programa que chame a API a partir da seguinte URL:

https://jsonplaceholder.typicode.com/posts/1Linguagem de código:  C#  ( cs )

A API retorna um objeto JSON com os quatro campos userId, id, titlee body.

{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}Linguagem de código:  C#  ( cs )

A seguir são mostradas as etapas para criar o programa que chama o endpoint da API acima:

Primeiro, crie uma Postclasse que possua os quatro campos correspondentes:

public class Post
{
    public int UserId { get; set; }
    public int Id { get; set;  }
    public string? Title  { get; set; }
    public string? Body  { get; set; }

    public override string ToString() => $"{Id} - {Title}";
}Linguagem de código:  C#  ( cs )

Na Postclasse, o ToString()método retorna uma string que consiste em Ide Titlede Post.

Segundo, crie uma interface chamada IPostServiceque tenha um método GetPost. O GetPostmétodo recebe uma postagem por um id e retorna um Postobjeto:

public interface IPostService
{
    Task<Post?> GetPost(int postId);
}Linguagem de código:  C#  ( cs )

Terceiro, crie uma PostServiceclasse que implemente a IPostServiceinterface:

public class PostService : IPostService
{
    private readonly HttpClient client;

    public PostService()
    {
        client = new HttpClient();
    }

    public async Task<Post?> GetPost(int postId)
    {
        var url = $"https://jsonplaceholder.typicode.com/posts/{postId}";
        var response = await client.GetAsync(url);

        if (response.IsSuccessStatusCode)
        {
            var responseBody = await response.Content.ReadAsStringAsync();

            var post = JsonSerializer.Deserialize<Post?>(
                responseBody,
                new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
            );
            return post;
        }
        else
        {
            throw new Exception($"Error: {response.StatusCode}");
        }
    }
}Linguagem de código:  C#  ( cs )

Na PostServiceclasse, o GetPostmétodo usa o HttpClientpara chamar a API para obter a resposta JSON e desserializa-a para o Postobjeto usando o JsonSerializere retorna o Post. O método gera uma exceção se ocorrer um erro.

Por fim, use a PostServiceclasse para chamar a API e exibir Postno console:

public class Program
{
    public static async Task Main(string[] args)
    {
        var postService = new PostService();
        try
        {
            var post = await postService.GetPost(1);
            Console.WriteLine(post);
        }
        catch (Exception)
        {
            throw;
        }
    }

}Linguagem de código:  C#  ( cs )

Juntando tudo.

using System.Text.Json;

namespace Decorator;

public class Post
{
    public int UserId { get; set; }
    public int Id { get; set;  }
    public string? Title  { get; set; }
    public string? Body  { get; set; }
    public override string ToString() => $"{Id} - {Title}";
}

public interface IPostService
{
    Task<Post?> GetPost(int postId);
}

public class PostService : IPostService
{
    private readonly HttpClient client;

    public PostService()
    {
        client = new HttpClient();
    }

    public async Task<Post?> GetPost(int postId)
    {
        var url = $"https://jsonplaceholder.typicode.com/posts/{postId}";
        var response = await client.GetAsync(url);

        if (response.IsSuccessStatusCode)
        {
            var responseBody = await response.Content.ReadAsStringAsync();

            var post = JsonSerializer.Deserialize<Post?>(
                responseBody,
                new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
            );
            return post;
        }
        else
        {
            throw new Exception($"Error: {response.StatusCode}");
        }
    }
}

public class Program
{
    public static async Task Main(string[] args)
    {
        var postService = new PostService();
        try
        {
            var post = await postService.GetPost(1);
            Console.WriteLine(post);
        }
        catch (Exception)
        {
            throw;
        }
    }

}Linguagem de código:  C#  ( cs )

Saída:

1 - sunt aut facere repellat provident occaecati excepturi optio reprehenderitLinguagem de código:  C#  ( cs )

O programa funciona conforme o esperado.

Agora, você recebe um novo requisito necessário para registrar a chamada de API. Para fazer isso você pode modificar a PostServiceclasse.

Mas, se fizer isso, violará o princípio da responsabilidade única . A PostServiceclasse deve ser responsável por chamar a API e retornar um arquivo Post. E não deve lidar com a funcionalidade de registro.

Para atender aos novos requisitos sem alterar a PostServiceclasse, você pode usar o padrão Decorator estendendo o PostServiceobjeto dinamicamente.

Adicionando um objeto decorador

Definiremos duas novas classes:

  • PostServiceDecoratorclasse que serve como Decoratorclasse
  • PostServiceLoggingDecoratorclasse que atua como a ConcreteDecoratorclasse:

Aqui está o novo diagrama UML:

Padrão de decorador C# - registro em log

Observe que não incluímos a Postclasse no diagrama para focar mais no padrão Decorator.

Primeiro, defina uma nova PostServiceDecoratorclasse abstrata que implemente a IPostServiceinterface e tenha uma IPostServiceinstância:

public abstract class PostServiceDecorator : IPostService
{
    protected readonly IPostService postService;

    public PostServiceDecorator(IPostService postService)
    {
        this.postService = postService;
    }

    public abstract Task<Post?> GetPost(int postId);

}Linguagem de código:  C#  ( cs )

Segundo, defina a PostLoggingDecoratorclasse que implementa a PostServiceDecoratorclasse:

public class PostServiceLoggingDecorator : PostServiceDecorator
{
    public PostServiceLoggingDecorator(IPostService postService)
        : base(postService) { }

    public async override Task<Post?> GetPost(int postId)
    {
        Console.WriteLine($"Calling the API to get the post with ID: {postId}");
        var stopwatch = Stopwatch.StartNew();

        try
        {
            var post = await postService.GetPost(postId);
            Console.WriteLine($"It took {stopwatch.ElapsedMilliseconds} ms to call the API");
            return post;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"GetPostAsync threw exception: {ex.Message}");
            throw;
        }
        finally
        {
            stopwatch.Stop();
        }

    }
}Linguagem de código:  C#  ( cs )

O GetPost()método de PostServiceLoggingDecoratorchama o GetPost()método da IPostServiceinstância. Ele também usa o objeto StopWatch para medir o tempo de chamada da API e registra algumas informações no console.

Em outras palavras, o GetPost()método da PostServiceLoggingDecoratorclasse adiciona a funcionalidade de registro ao GetPost()método do IPostServiceobjeto.

Terceiro, modifique a Programclasse para usar a PostLoggingDecoratorclasse:

public class Program
{
    public static async Task Main(string[] args)
    {
        IPostService postService = new PostService();
        var postServiceLogging = new PostServiceLoggingDecorator(postService);
        try
        {
            var post = await postServiceLogging.GetPost(1);
            Console.WriteLine(post);
        }
        catch (Exception)
        {
            throw;
        }
    }

}Linguagem de código:  C#  ( cs )

No Main()método, criamos uma instância do PostServicee a passamos para o construtor do PostServiceLoggingDecorator.

O PostLoggingDecoratoratua como um decorador para o PostServiceobjeto, adicionando a funcionalidade de registro ao PostServiceobjeto.

Junte tudo.

using System.Diagnostics;
using System.Text.Json;

namespace Decorator;

public class Post
{
    public int UserId { get; set; }
    public int Id { get; set;  }
    public string? Title  { get; set; }
    public string? Body  { get; set; }
    public override string ToString() => $"{Id} - {Title}";
}

public interface IPostService
{
    Task<Post?> GetPost(int postId);
}


public class PostService : IPostService
{
    private readonly HttpClient client;

    public PostService()
    {
        client = new HttpClient();
    }

    public async Task<Post?> GetPost(int postId)
    {
        var url = $"https://jsonplaceholder.typicode.com/posts/{postId}";
        var response = await client.GetAsync(url);

        if (response.IsSuccessStatusCode)
        {
            var responseBody = await response.Content.ReadAsStringAsync();

            var post = JsonSerializer.Deserialize<Post?>(
                responseBody,
                new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
            );
            return post;
        }
        else
        {
            throw new Exception($"Error: {response.StatusCode}");
        }
    }
}

public abstract class PostServiceDecorator : IPostService
{
    protected readonly IPostService postService;

    public PostServiceDecorator(IPostService postService)
    {
        this.postService = postService;
    }

    public abstract Task<Post?> GetPost(int postId);

}

public class PostServiceLoggingDecorator : PostServiceDecorator
{
    public PostServiceLoggingDecorator(IPostService postService)
        : base(postService) { }

    public async override Task<Post?> GetPost(int postId)
    {
        Console.WriteLine($"Calling the API to get the post with ID: {postId}");
        var stopwatch = Stopwatch.StartNew();

        try
        {

            var post = await postService.GetPost(postId);
            Console.WriteLine($"It took {stopwatch.ElapsedMilliseconds} ms to call the API");
            return post;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"GetPostAsync threw exception: {ex.Message}");
            throw;
        }
        finally
        {
            stopwatch.Stop();
        }

    }
}

public class Program
{
    public static async Task Main(string[] args)
    {
        IPostService postService = new PostService();
        var postServiceLogging = new PostServiceLoggingDecorator(postService);
        try
        {
            var post = await postServiceLogging.GetPost(1);
            Console.WriteLine(post);
        }
        catch (Exception)
        {
            throw;
        }
    }

}Linguagem de código:  C#  ( cs )

Neste exemplo, começamos com o PostServiceobjeto:

E decore-o com o PostServiceLoggingDecoratorobjeto adicionando a funcionalidade de registro:

Adicionando mais objeto decorador

Suponha que você queira melhorar o desempenho da PostServiceclasse armazenando em cache o resultado da chamada de API.

Por exemplo, se uma postagem com id 1 for solicitada pela primeira vez, você poderá chamar diretamente a API e salvá-la em um cache. Mas se a mesma postagem for solicitada novamente, você poderá recuperar o Postcom id 1 do cache em vez de fazer uma chamada para a API.

Para fazer isso, você pode adicionar uma nova classe de decorador chamada PostServiceCacheque realiza o cache:

Padrão de decorador C# – cache

Primeiro, defina o PostServiceCacheDecoratorque estende o PostServiceDecorator:

public class PostServiceCacheDecorator : PostServiceDecorator
{
    private readonly Dictionary<int, Post> cache;

    public PostServiceCacheDecorator(IPostService postService) : base(postService)
    {
        cache = new Dictionary<int, Post>();
    }

    public async override Task<Post?> GetPost(int postId)
    {
        // get post from the cache
        if (cache.TryGetValue(postId, out var post))
        {
            // demo purpose 
            Console.WriteLine($"Getting the post with id {postId} from the cache");
            return post;
        }

        // otherwise call the API
        post = await postService.GetPost(postId);
        if (post != null)
        {
            cache[postId] = post;
        }
        return post;

    }
}Linguagem de código:  C#  ( cs )

A PostServiceCacheDecoratorclasse possui um membro cacheque serve como cache. O tipo de cacheé Dictionary<int, Post>que permite pesquisar Postpor id.

O GetPost()método da PostServiceCacheDecoratorclasse verifica o cache em busca do id solicitado e retorna Postse já estiver no cache. Caso contrário, ele usa o GetPost()método do PostServiceobjeto para chamar a API e adiciona o resultado ao cache.

Segundo, modifique o Programpara usar o PostServiceCacheDecoratorque armazena em cache o resultado da chamada de API se o usuário solicitar a mesma postagem novamente:

public class Program
{
    public static async Task Main(string[] args)
    {
        IPostService postService = new PostService();
        var postServiceLogging = new PostServiceLoggingDecorator(postService);
        var postServiceCache = new PostServiceCacheDecorator(postServiceLogging);
        try
        {
            var post = await postServiceCache.GetPost(1);
            Console.WriteLine(post);

            // request the same post second time
            Console.WriteLine("Getting the same post again:");
            post = await postServiceCache.GetPost(1);
            Console.WriteLine(post);
        }
        catch (Exception)
        {
            throw;
        }
    }

}Linguagem de código:  C#  ( cs )

Saída:

Calling the API to get the post with ID: 1
It took 1698 ms to call the API
1 - sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Getting the same post again:
Getting the post with id 1 from the cache
1 - sunt aut facere repellat provident occaecati excepturi optio reprehenderitLinguagem de código:  C#  ( cs )

A saída mostra que a segunda chamada ao GetPostmétodo não chama a API, mas obtém o resultado do cache.

Neste exemplo, o PostServiceLoggingDecorator decora o PostService e o PotServiceCacheDecorator decora a classe PostServiceLoggingDecorator.

Padrão de decorador C# - PostServiceCacheDecorator

Se quiser, você pode adicionar mais classes de decoradores e eles decoram uns aos outros para adicionar mais funcionalidade aos objetos decorados.

Além disso, você pode misturar e combinar o decorador com base nos requisitos. Isso torna seu código mais flexível.

Padrão de decorador em .NET

O .NET usa o padrão decorador em algumas bibliotecas. Por exemplo, as BufferedStreamclasses GZipStream, e CryptoStreamsão os decoradores da Streamclasse. Os FileStream, MemoryStreame NetworkStreamsão Streamclasses concretas.

Observe que as classes Streame FileStreampossuem mais métodos do que os listados no diagrama. Apresentamos o método Reade Writeapenas para fins de simplificação.

Resumo

  • Use o padrão decorador C# para estender o comportamento de um objeto dinamicamente em tempo de execução sem usar herança.

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *