MÓDULO 05 · CONCEITO 03 DE 14

Decorator pattern & higher-order functions

A base funcional do AOP "sem framework". Do GoF (1994) ao @decorator do Python, do func(http.Handler) http.Handler do Go aos proxies de C#. Por que envolver é, no fim das contas, a operação mais útil em design.

Tempo de leitura ~22 min Pré-requisito Conceitos 01 e 02 (cross-cutting + AOP) Próximo Middleware HTTP — pipeline com next

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.

C# — decorator com classe (DI-friendly)
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.

Python — decorator de funçã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.

Go — função genérica que envelopa outra
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.

armadilha em produção

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.

heurística do sênior

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

  1. 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 ordem Logging(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.
  2. 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".
  3. 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); chi middleware (github.com/go-chi/chi); Polly v8 ResiliencePipeline (.NET); Scrutor Decorate extension (.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

  1. livro Design Patterns: Elements of Reusable Object-Oriented Software — Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (Addison-Wesley, 1994). O original. Capítulo 4 (Structural Patterns) traz Decorator com motivação, estrutura, exemplo e consequências. Trinta anos depois, a página continua atual sem alterações.
  2. livro Head First Design Patterns (2ª ed.) — Eric Freeman, Elisabeth Robson (O'Reilly, 2020). Capítulo 3 (Decorator) é a melhor introdução pedagógica a decorator que existe. Para quem nunca formalizou o padrão, vale ler antes do GoF.
  3. livro Functional Programming in C# (2ª ed.) — Enrico Buonanno (Manning, 2022). Capítulos 7 e 9 cobrem higher-order functions e composição em C# moderno — a forma funcional do decorator com tipagem completa via Func/Action.
  4. livro Fluent Python (2ª ed.) — Luciano Ramalho (O'Reilly, 2022). Capítulo 9 (Decorators and Closures) é o tratamento mais didático e completo de decorators Python em livro — incluindo decorators com parâmetros, decorators de classe e o padrão de três níveis.
  5. livro Concurrency in Go — Katherine Cox-Buday (O'Reilly, 2017). Apêndice e capítulos sobre patterns mostram como Go usa higher-order para o que outras linguagens chamam de decorator. Especialmente útil para entender a cultura local.
  6. livro Code That Fits in Your Head — Mark Seemann (Addison-Wesley, 2021). Cap. 11 e 12 tratam decorator em C# como ferramenta principal de cross-cutting — Seemann é um dos defensores mais articulados da abordagem "decorator manual" sobre "AOP framework".
  7. artigo PEP 318 — Decorators for Functions and Methods — Anthony Baxter (2003). peps.python.org/pep-0318 — A proposta original que introduziu @decorator em Python 2.4. Curta, precisa, com a motivação histórica.
  8. artigo Writing Idiomatic Decorators in Python — Graham Dumpleton (blog series, 2014–2016). graham-dumpleton.me/blog — Dumpleton, autor da biblioteca wrapt, escreveu uma série exaustiva sobre as armadilhas de decorators em Python. Indispensável para entender as sutilezas além do @wraps.
  9. artigo Aspect-Oriented Programming with the Decorator Pattern — Mark Seemann (blog post, 2010). blog.ploeh.dk — Seemann argumenta que decorator manual via DI cobre 95% dos casos onde se usa AOP framework. Texto curto, conexão direta com o conceito 02.
  10. docs Python — functools.wraps. docs.python.org/3/library/functools.html#functools.wraps — Documentação oficial. Uma página, leitura obrigatória para qualquer um que vá escrever decorator Python.
  11. docs Scrutor — Decorator extension for Microsoft.Extensions.DependencyInjection. github.com/khellang/Scrutor — Documentação da biblioteca que adiciona services.Decorate ao container DI do .NET. Ler o código fonte é didático: é Decorator GoF puro em ~50 linhas.
  12. vídeo The Functional Programmer's Toolkit — Scott Wlaschin (NDC, 2019). YouTube. Wlaschin explica higher-order functions como ferramenta fundamental — bom contraponto cultural ao decorator OO. Aplicável a F#, mas as ideias migram trivialmente para C#, Python e Go.