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. PodeComponent
ser uma interface ou uma classe abstrata . OComponent
define um objeto que será decorado.ConcreteComponent
: Esta é a classe que implementa aComponent
interface.Decorator
: esta é uma classe abstrata que implementa aComponent
interface e contém uma referência aoComponent
objeto.ConcreteDecorator
: esta é a classe que estende aDecorator
classe e adiciona comportamento adicional aoComponent
objeto.
Neste diagrama, a Decorator
classe 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/1
Linguagem de código: C# ( cs )
A API retorna um objeto JSON com os quatro campos userId
, id
, title
e 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 Post
classe 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 Post
classe, o ToString()
método retorna uma string que consiste em Id
e Title
de Post
.
Segundo, crie uma interface chamada IPostService
que tenha um método
. O GetPost
método recebe uma postagem por um id e retorna um GetPost
Post
objeto:
public interface IPostService
{
Task<Post?> GetPost(int postId);
}
Linguagem de código: C# ( cs )
Terceiro, crie uma PostService
classe que implemente a IPostService
interface:
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 PostService
classe, o GetPost
método usa o HttpClient
para chamar a API para obter a resposta JSON e desserializa-a para o Post
objeto usando o JsonSerializer
e retorna o Post
. O método gera uma exceção se ocorrer um erro.
Por fim, use a PostService
classe para chamar a API e exibir Post
no 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 reprehenderit
Linguagem 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 PostService
classe.
Mas, se fizer isso, violará o princípio da responsabilidade única . A PostService
classe 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 PostService
classe, você pode usar o padrão Decorator estendendo o PostService
objeto dinamicamente.
Adicionando um objeto decorador
Definiremos duas novas classes:
PostServiceDecorator
classe que serve comoDecorator
classePostServiceLoggingDecorator
classe que atua como aConcreteDecorator
classe:
Aqui está o novo diagrama UML:
Observe que não incluímos a Post
classe no diagrama para focar mais no padrão Decorator.
Primeiro, defina uma nova PostServiceDecorator
classe abstrata que implemente a
interface e tenha uma IPostService
instância:IPostService
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 PostLoggingDecorator
classe que implementa a PostServiceDecorator
classe:
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
método de GetPost()
PostServiceLoggingDecorator
chama o
método da GetPost()
IPostService
instâ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 PostServiceLoggingDecorator
classe adiciona a funcionalidade de registro ao GetPost()
método do IPostService
objeto.
Terceiro, modifique a Program
classe para usar a PostLoggingDecorator
classe:
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 PostService
e a passamos para o construtor do PostServiceLoggingDecorator
.
O PostLoggingDecorator
atua como um decorador para o
objeto, adicionando a funcionalidade de registro ao PostService
objeto.PostService
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 PostService
objeto:
E decore-o com o PostServiceLoggingDecorator
objeto adicionando a funcionalidade de registro:
Adicionando mais objeto decorador
Suponha que você queira melhorar o desempenho da PostService
classe 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 Post
com 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 PostServiceCache
que realiza o cache:
Primeiro, defina o PostServiceCacheDecorator
que 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 PostServiceCacheDecorator
classe possui um membro cache
que serve como cache. O tipo de cache
é Dictionary<int, Post>
que permite pesquisar Post
por id.
O
método da GetPost()
PostServiceCacheDecorator
classe verifica o cache em busca do id solicitado e retorna Post
se já estiver no cache. Caso contrário, ele usa o
método do GetPost()
PostService
objeto para chamar a API e adiciona o resultado ao cache.
Segundo, modifique o Program
para usar o PostServiceCacheDecorator
que 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 reprehenderit
Linguagem de código: C# ( cs )
A saída mostra que a segunda chamada ao GetPost
mé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.
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 BufferedStream
classes GZipStream
, e CryptoStream
são os decoradores da Stream
classe. Os FileStream
, MemoryStream
e NetworkStream
são Stream
classes concretas.
Observe que as classes Stream
e FileStream
possuem mais métodos do que os listados no diagrama. Apresentamos o método Read
e Write
apenas 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.