Em 1994, na primeira edição de Design Patterns: Elements of Reusable Object-Oriented Software, Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides — a "Gang of Four" — descreveram vinte e três padrões clássicos. Um deles, listado na categoria "estrutural", recebeu um nome modesto: Decorator. A descrição do GoF é precisa: "anexar responsabilidades adicionais a um objeto dinamicamente; decorators provêem uma alternativa flexível à herança para extensão de funcionalidade". Em 1994, ninguém previa que esse padrão seria a base operacional de quase todo cross-cutting concern moderno — mas ele foi, e ainda é.
A intuição é desconcertantemente simples. Você tem um objeto que faz X. Você quer que o objeto faça X e mais alguma coisa — sem mudar o objeto. Solução: cria-se um segundo objeto que envelopa o primeiro, expõe a mesma interface, e, ao receber chamada, decide quando delegar para o original e quando agregar comportamento próprio. O cliente que usa o decorator não distingue entre ele e o objeto interno; a interface é a mesma. A diferença está no comportamento agregado. Isso é o padrão.
Onze anos depois, em programação funcional, a mesma ideia aparece sem precisar do vocabulário de objeto: você tem uma função que faz X. Você quer uma função que faça X e mais alguma coisa. Cria-se uma função de ordem superior — uma higher-order function — que recebe a função original e retorna uma nova função que envolve a primeira. O nome da operação varia (decorator no Python, middleware no Go, wrapper em C#, monad transformer em Haskell), mas a estrutura é a mesma do GoF: envolver para acrescentar.
Este conceito é a base das ferramentas que aparecem nos próximos
onze. Middleware HTTP é decorator de Handler;
interceptor é decorator de método; behavior MediatR é decorator
de handler; Depends de FastAPI compõe via
decoração no nível de função. Quem entende decorator + higher-order
function consegue ler quase qualquer biblioteca AOP-like sem
precisar de manual — porque o que muda é só o sabor sintático.
O padrão original — Decorator GoF
O Design Patterns original ilustra Decorator com um
exemplo de janelas gráficas: VisualComponent é a
interface; TextView é uma implementação concreta;
ScrollDecorator, BorderDecorator e
ShadowDecorator são decorators que adicionam
comportamento envolvendo qualquer VisualComponent.
Você compõe pilhas: new ScrollDecorator(new
BorderDecorator(new TextView())). Cada camada chama a
anterior; a pilha resultante se comporta como
VisualComponent.
O padrão tem três peças: a interface comum
(VisualComponent) que tanto o decorator quanto o
decorado implementam; o componente concreto
(TextView) que faz o trabalho real; e o
decorator abstrato que delega para o
componente interno e permite que decorators concretos adicionem
comportamento antes, depois ou em torno da delegação.
A virtude do decorator sobre herança é a composição em runtime.
Com herança, você define em compile-time que BorderTextView
extends TextView; combinar sete decorations vira uma
hierarquia explosiva de classes. Com decorator, você compõe os
sete em qualquer ordem em runtime, e a combinação é um objeto
novo sem precisar de classe nova. Esse poder combinatório é o
que torna o padrão tão útil para cross-cutting concerns: você
configura no startup quais decorators envolvem quais serviços, e
a combinação resultante atende às várias políticas sem que a
classe de serviço saiba.
Decorator clássico em C#
public interface IPedidoService
{
Task<Pedido> Criar(CriarPedidoCmd cmd, CancellationToken ct);
}
// componente concreto: a regra de negócio
public class PedidoService : IPedidoService
{
public Task<Pedido> Criar(CriarPedidoCmd cmd, CancellationToken ct)
=> Task.FromResult(new Pedido(cmd.ClienteId, cmd.Itens));
}
// decorator: adiciona log antes/depois sem alterar PedidoService
public class LoggingPedidoService : IPedidoService
{
private readonly IPedidoService _inner;
private readonly ILogger _log;
public LoggingPedidoService(IPedidoService inner, ILogger<LoggingPedidoService> log)
=> (_inner, _log) = (inner, log);
public async Task<Pedido> Criar(CriarPedidoCmd cmd, CancellationToken ct)
{
_log.LogInformation("criando pedido {Cmd}", cmd);
try
{
var p = await _inner.Criar(cmd, ct);
_log.LogInformation("pedido criado {Id}", p.Id);
return p;
}
catch (Exception ex)
{
_log.LogError(ex, "falha ao criar pedido");
throw;
}
}
}
// composição via DI
services.AddScoped<IPedidoService, PedidoService>();
services.Decorate<IPedidoService, LoggingPedidoService>(); // Scrutor
LoggingPedidoService implementa
IPedidoService, recebe outro IPedidoService
no construtor (que pode ser o concreto ou outro decorator), e
delega adicionando o comportamento de log. A linha
services.Decorate da biblioteca Scrutor configura o
container DI para envolver o registro original com o decorator
— o resto do sistema continua pedindo IPedidoService
e recebe a pilha completa.
Higher-order functions — o decorator sem objeto
Em linguagens com funções de primeira classe — Python, JavaScript,
Go, Rust, e C# moderno via Func/Action —
o decorator não precisa de classe. Uma função que recebe outra
função como parâmetro e retorna uma terceira já é um decorator.
O termo correto vem da matemática: uma função cujos argumentos
ou retorno são funções é uma higher-order function
(HOF). O conceito é antigo: aparece em Lisp (1958), em ML
(1973), em Haskell (1990), e voltou ao mainstream com o
map/filter/reduce dos anos 2010.
A virtude da forma funcional é a brevidade. O exemplo C# acima gasta vinte linhas para envelopar um método; em Python, gasta seis. A perda é o tipo de retorno — em linguagens dinâmicas, o compilador não te lembra de manter a assinatura. Em Go, com generics (1.18+, 2022), você consegue brevidade com tipagem; em Rust e C# moderno também. Higher-order é o nome genérico, decorator é o nome do padrão quando aplicado a cross-cutting concern, middleware é o nome quando aplicado a pipeline HTTP. Estrutura idêntica, três rótulos.
O @decorator do Python
Python introduziu a sintaxe @decorator em 2003, na
versão 2.4, via PEP 318 — proposto por Michele Simionato e
Anthony Baxter. A sintaxe é açúcar para uma reatribuição: a
anotação @trace antes de def f():
equivale a f = trace(f) imediatamente após a
definição. Não há mágica em runtime — só renomeação. É um dos
melhores casos de açúcar sintático na história da linguagem,
porque transformou um padrão funcional em algo legível.
import logging
from functools import wraps
log = logging.getLogger(__name__)
def trace(fn):
@wraps(fn) # preserva __name__/__doc__
def wrapper(*args, **kwargs):
log.info("entrando em %s", fn.__qualname__)
try:
result = fn(*args, **kwargs)
log.info("saindo de %s ok", fn.__qualname__)
return result
except Exception:
log.exception("falha em %s", fn.__qualname__)
raise
return wrapper
@trace
def calcular_frete(cep: str, peso_kg: float) -> float:
return tabela.lookup(cep) * peso_kg
O @wraps(fn) da functools é detalhe
importante: sem ele, a função decorada perde
__name__, __doc__ e
__module__, o que confunde stack traces e
ferramentas como Sphinx ou pytest. functools.wraps
copia esses metadados do original para o wrapper. Ler decorators
Python sem @wraps é sinal de inexperiência —
bibliotecas sérias sempre o aplicam.
Decorator com parâmetros — uma camada a mais
Quando o decorator precisa de configuração (timeout, número de tentativas, namespace de métrica), há mais uma camada de indireção: a função externa recebe os parâmetros e retorna o decorator de fato. Esse padrão de três níveis é onde mais gente tropeça em Python.
def retry(times: int, delay: float = 0.1):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return fn(*args, **kwargs)
except Exception:
if attempt == times - 1:
raise
time.sleep(delay * (2 ** attempt)) # backoff
return wrapper
return decorator
@retry(times=3, delay=0.2)
def chamar_fornecedor(payload):
...
Três níveis: retry(3, 0.2) retorna o
decorator, que recebe fn e retorna
wrapper, que recebe os argumentos e roda a lógica.
O fluxo é mecânico, mas precisa ser desenhado no quadro pelo
menos uma vez para fixar.
Middleware funcional em Go — func(http.Handler) http.Handler
Go não tem decorator como sintaxe, mas tem uma convenção quase
universal para a mesma operação. A biblioteca padrão define
http.Handler como interface com um único método
ServeHTTP(w, r). Middleware em Go é, por convenção,
uma função que recebe http.Handler e retorna
http.Handler. Essa assinatura permite encadeamento
de qualquer profundidade.
// middleware: registra duração de cada request
func WithTiming(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
started := time.Now()
next.ServeHTTP(w, r) // delega ao próximo da cadeia
slog.InfoContext(r.Context(),
"request",
"method", r.Method,
"path", r.URL.Path,
"duration_ms", time.Since(started).Milliseconds(),
)
})
}
// middleware: injeta correlation ID
func WithCorrelationID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Correlation-ID")
if id == "" {
id = newID()
}
ctx := context.WithValue(r.Context(), correlationKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// composição manual ou via chi.Use
handler := WithCorrelationID(WithTiming(myAppHandler))
http.ListenAndServe(":8080", handler)
Note como cada middleware é um decorator: recebe um
http.Handler, devolve outro que tem a mesma
interface (porque http.HandlerFunc implementa
http.Handler), e adiciona comportamento ao redor da
delegação. A composição é manual — sem framework, sem mágica;
só funções compondo funções. chi, echo,
gin e bibliotecas similares fornecem só açúcar
sintático em torno dessa convenção.
O mesmo concern, três sintaxes
Para fixar a equivalência conceitual entre as três formas, considere o caso clássico: cachear o resultado de uma função por chave. Cada linguagem expressa o cache decorator de um jeito que reflete a cultura local.
public interface ICatalogoService
{
Task<Produto?> ObterPorId(string id, CancellationToken ct);
}
public class CatalogoServiceCache : ICatalogoService
{
private readonly ICatalogoService _inner;
private readonly IMemoryCache _cache;
private static readonly TimeSpan TTL = TimeSpan.FromMinutes(5);
public CatalogoServiceCache(ICatalogoService inner, IMemoryCache cache)
=> (_inner, _cache) = (inner, cache);
public async Task<Produto?> ObterPorId(string id, CancellationToken ct)
{
if (_cache.TryGetValue<Produto>(id, out var hit)) return hit;
var produto = await _inner.ObterPorId(id, ct);
if (produto is not null)
_cache.Set(id, produto, TTL);
return produto;
}
}
// startup
services.AddScoped<ICatalogoService, CatalogoService>();
services.Decorate<ICatalogoService, CatalogoServiceCache>();
C# moderno faz decorator com classe + interface +
Decorate da Scrutor. Tipagem completa, integra
com DI, fácil de mockar em teste. O custo é a verbosidade:
uma classe inteira para empacotar uma operação.
from functools import wraps
from cachetools import TTLCache
cache: TTLCache = TTLCache(maxsize=10_000, ttl=300)
def cached_by_id(fn):
@wraps(fn)
async def wrapper(self, id: str):
if id in cache:
return cache[id]
produto = await fn(self, id)
if produto is not None:
cache[id] = produto
return produto
return wrapper
class CatalogoService:
@cached_by_id
async def obter_por_id(self, id: str) -> Produto | None:
...
Python escolhe brevidade. Sem interface, sem container DI — só decorator + cache externo. A simplicidade tem preço: chave de cache é convenção (nome do parâmetro), não contrato; testabilidade exige limpar o cache entre testes. A trocas que a cultura aceita.
type CatalogoService interface {
ObterPorID(ctx context.Context, id string) (*Produto, error)
}
func Cached(inner CatalogoService, ttl time.Duration) CatalogoService {
return &cachedCatalogo{
inner: inner,
ttl: ttl,
store: make(map[string]cacheEntry),
}
}
type cachedCatalogo struct {
inner CatalogoService
ttl time.Duration
mu sync.RWMutex
store map[string]cacheEntry
}
type cacheEntry struct {
produto *Produto
expira time.Time
}
func (c *cachedCatalogo) ObterPorID(ctx context.Context, id string) (*Produto, error) {
c.mu.RLock()
if e, ok := c.store[id]; ok && time.Now().Before(e.expira) {
c.mu.RUnlock()
return e.produto, nil
}
c.mu.RUnlock()
p, err := c.inner.ObterPorID(ctx, id)
if err != nil || p == nil {
return p, err
}
c.mu.Lock()
c.store[id] = cacheEntry{produto: p, expira: time.Now().Add(c.ttl)}
c.mu.Unlock()
return p, nil
}
// composição
service := Cached(NewCatalogoService(db), 5*time.Minute)
Go fica entre os dois: explícito como C#, sem framework
como Python. A struct decorada implementa a mesma interface
do componente interno. A composição é uma função
Cached(...) chamada onde o serviço é
montado — geralmente em main.go.
Composição e ordem — a sutileza que ninguém te ensina
Empilhar decorators é a operação que define o comportamento
final, e a ordem importa. Considere três decorators
conhecidos: Logging, Cached,
Retry. As composições Logging(Cached(Retry(s)))
e Cached(Logging(Retry(s))) produzem
comportamentos distintos:
Em Logging(Cached(Retry(s))), o log é o mais externo:
ele registra cada chamada, mesmo as que retornam do
cache sem chegar ao serviço. Se o cache hit é frequente, o log
vai ter altíssimo volume com mensagens "ok rápido". Em
Cached(Logging(Retry(s))), o cache é externo: chamadas
que batem no cache não passam pelo log de jeito nenhum, e o
log só registra as que efetivamente foram à camada interna.
Volume menor, mas você perde visibilidade sobre cache hits.
Não há ordem "certa" universal. Há ordem certa para o que você quer observar. Esse é um dos exercícios de design mais genuínos de cross-cutting concerns: pensar a pilha como sequência ordenada, justificar por que cada camada está onde está, e documentar a decisão. Times maduros têm um diagrama do pipeline em algum lugar canônico do repositório, justamente para que essas escolhas não se percam.
Pilha de decorators reordenada por engano. Em DI ou em configuração de middleware, basta inverter duas linhas para que o sistema continue compilando, continue passando nos testes unitários, e em produção comece a ter comportamento sutilmente diferente — log faltando, retry antes de cache (cada hit reexecuta retry), autorização depois de logging do payload (vaza dado sensível em log de request não autorizado). Toda mudança de ordem em pilha de cross-cutting concern merece teste de integração explícito — e idealmente uma justificativa em revisão.
Quando decorator é a ferramenta certa — e quando não
Decorator brilha em três situações concretas. A primeira é cross-cutting concern bem-definido sobre uma interface estável — logging, métrica, retry, cache, auditoria sobre serviços de domínio. A segunda é experimentação: você consegue testar uma nova política de retry trocando o decorator no startup, sem tocar o serviço base. A terceira é teste: decorators de mock (que retornam respostas sintéticas) ou de spy (que registram chamadas) compõem com o resto, e o sistema sob teste não precisa saber.
Decorator perde quando há quatro situações. Primeiro, quando o número de decorators começa a passar de cinco ou seis na mesma pilha — a leitura vira difícil, a ordem vira frágil. Segundo, quando os decorators precisam conversar entre si: o de retry quer saber se o de circuit breaker está aberto, por exemplo. Aí composição linear não basta, e vale formalizar o que está implícito (uma resilience policy nomeada, conceito 09). Terceiro, quando a interface do componente interno é instável — toda mudança força mudar todos os decorators. Quarto, quando o cross-cutting concern só se aplica a alguns métodos da interface, não todos — decorator acaba tendo lógica de "se for este método, faço; se for aquele, delego direto", o que é tangling disfarçado.
Quando montar um decorator, escreva no comentário (ou no PR) a frase: "este decorator agrega X, sobre Y, antes/depois/em torno de Z". Se não consegue completar a frase em uma linha, o decorator está fazendo coisa demais. Se cinco decorators na pilha não cabem cada um numa linha assim, a pilha está pedindo refatoração — agrupar dois que andam sempre juntos, virar política nomeada, ou subir para abstração mais forte como pipeline declarativo.
Por que importa para a sua carreira
A maior parte do código "framework-y" que você vai ler em bibliotecas de produção — Polly, Tenacity, FastAPI, ASP.NET Core, MediatR, Spring, Express, chi — é decorator embaixo de açúcar sintático. Reconhecer o padrão por trás da sintaxe é o que te permite migrar de framework sem reaprender a cabeça — você reconhece "isso é a pilha de decorators X envolvendo o handler Y" e segue. Em entrevistas técnicas, "implemente retry/cache/logging como decorator" é exercício comum, e a resposta forte mostra três coisas: a estrutura básica do wrapper, o uso de higher-order/closures, e — esta separa pleno de sênior — a discussão de ordem na pilha. Em design de sistema, ter "decorator" como verbo no vocabulário ativo muda a qualidade da conversa: "vamos colocar isso como decorator do repositório" é uma proposta concreta; "vamos adicionar logging" é uma proposta vaga.
Como praticar
-
Implemente os três decorators canônicos em três
linguagens. Escolha logging, cache e retry. Implemente
cada um em C# (com classe + Scrutor), Python (com
@decorator), e Go (com função que envelopa). O componente interno é livre — qualquer serviço com método async que retorna um valor. Compose os três na ordemLogging(Cached(Retry(svc)))e teste o comportamento em quatro cenários: hit de cache, miss com sucesso na primeira tentativa, miss com retry, miss com falha definitiva. Documente o que cada cenário emite em log e onde cada decorator atua. -
Inverta a pilha e meça o impacto. Use a
mesma implementação do exercício 1, mas troque a ordem para
Cached(Logging(Retry(svc))). Rode os mesmos quatro cenários e documente as diferenças de log, métrica, e comportamento observável. Faça uma terceira variação onde você inverte retry e cache (Logging(Retry(Cached(svc)))). Discuta qual ordem você defenderia para um sistema real, e por quê. Esse é o exercício que separa "uso decorator" de "entendo decorator". -
Anatomia de uma biblioteca real. Escolha uma
das seguintes bibliotecas e identifique no código fonte onde
está o padrão decorator:
functools.lru_cache(Python stdlib);chimiddleware (github.com/go-chi/chi); Polly v8ResiliencePipeline(.NET); ScrutorDecorateextension (.NET). Você quase sempre vai encontrar a estrutura "função que recebe e retorna função do mesmo tipo" ou "tipo que envolve outro do mesmo tipo". Escreva uma página resumindo o padrão dentro de cada biblioteca — esse é um documento que vale como cartão de visita técnico.
Referências para aprofundar
- livro Design Patterns: Elements of Reusable Object-Oriented Software — Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (Addison-Wesley, 1994).
- livro Head First Design Patterns (2ª ed.) — Eric Freeman, Elisabeth Robson (O'Reilly, 2020).
- livro Functional Programming in C# (2ª ed.) — Enrico Buonanno (Manning, 2022).
- livro Fluent Python (2ª ed.) — Luciano Ramalho (O'Reilly, 2022).
- livro Concurrency in Go — Katherine Cox-Buday (O'Reilly, 2017).
- livro Code That Fits in Your Head — Mark Seemann (Addison-Wesley, 2021).
- artigo PEP 318 — Decorators for Functions and Methods — Anthony Baxter (2003).
- artigo Writing Idiomatic Decorators in Python — Graham Dumpleton (blog series, 2014–2016).
- artigo Aspect-Oriented Programming with the Decorator Pattern — Mark Seemann (blog post, 2010).
- docs Python — functools.wraps.
- docs Scrutor — Decorator extension for Microsoft.Extensions.DependencyInjection.
- vídeo The Functional Programmer's Toolkit — Scott Wlaschin (NDC, 2019).