Design Patterns

Decorator

Adiciona responsabilidades a um objeto dinamicamente, sem alterar sua classe

Conceito e intenção

Decorator adiciona comportamentos a um objeto individualmente, envolvendo-o com um wrapper que implementa a mesma interface. O padrão é a alternativa à herança quando você precisa combinar comportamentos de forma flexível, sem explodir hierarquias de classes.

O problema motivador é a explosão combinatória da herança. Imagine um repositório que pode ter cache, logging, métricas e retry. Com herança, você precisaria de CachedRepository, LoggedRepository, MetricedRepository, e todas as combinações: CachedLoggedRepository, CachedMetricedRepository, etc. — N comportamentos = 2^N classes. Com Decorator, você empilha comportamentos em runtime: new Retry(new Logged(new Cached(new JpaRepo()))).

A chave é que cada Decorator implementa a mesma interface que o componente que envolve. O cliente não sabe se está falando com o objeto real ou com um wrapper — transparência total.


Estrutura

┌──────────────────┐
│   <<interface>>  │◀─────────────────────────────┐
│    Component     │                               │
│  + operacao()    │                               │
└──────────────────┘                               │
        ▲                                          │
        │                               ┌──────────┴───────┐
┌───────┴──────────┐                    │    Decorator     │
│ConcreteComponent │                    │  - component     │──has-a──▶ Component
│  + operacao()    │                    │  + operacao()    │
└──────────────────┘                    └──────────────────┘

                                    ┌───────────┴────────────┐
                             ┌──────┴──────┐         ┌───────┴──────┐
                             │ DecoratorA  │         │  DecoratorB  │
                             │+operacao()  │         │ +operacao()  │
                             └─────────────┘         └──────────────┘

Participantes:

  • Component — interface comum ao objeto real e a todos os decorators
  • ConcreteComponent — implementação base, a lógica central
  • Decorator — classe abstrata que contém um Component e delega para ele
  • ConcreteDecorator — adiciona comportamento antes/depois de delegar

Implementação Java — repositório com cross-cutting concerns

// Interface base (Component)
public interface ProductRepository {
    Optional<Product> findById(String id);
    List<Product> findAll(ProductFilter filter);
    void save(Product product);
    void delete(String id);
}

// Implementação base (ConcreteComponent)
public class JpaProductRepository implements ProductRepository {
    private final EntityManager em;

    public JpaProductRepository(EntityManager em) {
        this.em = em;
    }

    @Override
    public Optional<Product> findById(String id) {
        return Optional.ofNullable(em.find(ProductEntity.class, id))
            .map(ProductMapper::toDomain);
    }

    @Override
    public List<Product> findAll(ProductFilter filter) {
        // query JPQL real
        return List.of();
    }

    @Override
    public void save(Product product) {
        em.merge(ProductMapper.toEntity(product));
    }

    @Override
    public void delete(String id) {
        em.remove(em.find(ProductEntity.class, id));
    }
}

// Decorator de cache (adiciona cache sem tocar em JpaProductRepository)
public class CachedProductRepository implements ProductRepository {
    private final ProductRepository delegate;
    private final Cache<String, Product> cache;

    public CachedProductRepository(ProductRepository delegate, Cache<String, Product> cache) {
        this.delegate = delegate;
        this.cache    = cache;
    }

    @Override
    public Optional<Product> findById(String id) {
        Product cached = cache.getIfPresent(id);
        if (cached != null) return Optional.of(cached);

        Optional<Product> found = delegate.findById(id);
        found.ifPresent(p -> cache.put(id, p));
        return found;
    }

    @Override
    public List<Product> findAll(ProductFilter filter) {
        return delegate.findAll(filter); // cache de listas é complexo — delega
    }

    @Override
    public void save(Product product) {
        delegate.save(product);
        cache.put(product.getId(), product); // atualiza cache na escrita
    }

    @Override
    public void delete(String id) {
        delegate.delete(id);
        cache.invalidate(id); // invalida cache na remoção
    }
}

// Decorator de métricas
public class MeteredProductRepository implements ProductRepository {
    private final ProductRepository delegate;
    private final MeterRegistry registry;

    public MeteredProductRepository(ProductRepository delegate, MeterRegistry registry) {
        this.delegate = delegate;
        this.registry = registry;
    }

    @Override
    public Optional<Product> findById(String id) {
        return Timer.builder("repo.findById")
            .tag("entity", "product")
            .register(registry)
            .record(() -> delegate.findById(id));
    }

    @Override
    public List<Product> findAll(ProductFilter filter) {
        return Timer.builder("repo.findAll")
            .tag("entity", "product")
            .register(registry)
            .record(() -> delegate.findAll(filter));
    }

    @Override
    public void save(Product product) {
        delegate.save(product);
        registry.counter("repo.save", "entity", "product").increment();
    }

    @Override
    public void delete(String id) {
        delegate.delete(id);
        registry.counter("repo.delete", "entity", "product").increment();
    }
}

// Decorator de retry (tenta novamente em falhas transitórias)
public class RetryProductRepository implements ProductRepository {
    private final ProductRepository delegate;
    private final int maxAttempts;
    private final Duration delay;

    public RetryProductRepository(ProductRepository delegate, int maxAttempts, Duration delay) {
        this.delegate    = delegate;
        this.maxAttempts = maxAttempts;
        this.delay       = delay;
    }

    @Override
    public Optional<Product> findById(String id) {
        return withRetry(() -> delegate.findById(id));
    }

    @Override
    public List<Product> findAll(ProductFilter filter) {
        return withRetry(() -> delegate.findAll(filter));
    }

    @Override
    public void save(Product product) {
        withRetry(() -> { delegate.save(product); return null; });
    }

    @Override
    public void delete(String id) {
        withRetry(() -> { delegate.delete(id); return null; });
    }

    private <T> T withRetry(Supplier<T> operation) {
        int attempt = 0;
        while (true) {
            try {
                return operation.get();
            } catch (TransientDataAccessException e) {
                if (++attempt >= maxAttempts) throw e;
                try { Thread.sleep(delay.toMillis()); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw e; }
            }
        }
    }
}

// Composição — empilha os decorators conforme necessário
ProductRepository repo = new MeteredProductRepository(
    new RetryProductRepository(
        new CachedProductRepository(
            new JpaProductRepository(entityManager),
            Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(5, MINUTES).build()
        ),
        3, Duration.ofMillis(100)
    ),
    meterRegistry
);

// A ordem importa:
// Métricas → Retry → Cache → JPA
// Métricas medem tentativas incluindo retries
// Retry opera sobre o cache (cache miss aciona retry para JPA)

A ordem dos decorators importa

// Cenário: métricas medem tempo total (incluindo tentativas de retry)
new MeteredRepo(new RetryRepo(new CachedRepo(new JpaRepo())))

// vs.

// Cenário: métricas medem apenas a tentativa final bem-sucedida
new RetryRepo(new MeteredRepo(new CachedRepo(new JpaRepo())))

Ao empilhar decorators, pense na semântica: o decorator mais externo é aplicado primeiro e último. Logging do request deve ser o mais externo para capturar tudo; autenticação deve vir antes de autorização.


Java I/O Streams — o Decorator mais famoso

O sistema de I/O do Java foi construído inteiramente com o padrão Decorator:

// InputStream é a interface Component
// FileInputStream é ConcreteComponent
// BufferedInputStream, GZIPInputStream, DataInputStream são Decorators

// Lê arquivo comprimido com buffer
InputStream stream =
    new DataInputStream(
        new BufferedInputStream(         // adiciona buffer
            new GZIPInputStream(         // adiciona descompressão
                new FileInputStream("data.gz")
            )
        )
    );

// Escrever arquivo comprimido com buffer
OutputStream out =
    new GZIPOutputStream(
        new BufferedOutputStream(
            new FileOutputStream("output.gz")
        )
    );

Implementação TypeScript

// Interface base
interface ProductRepository {
  findById(id: string): Promise<Product | null>;
  findAll(filter: ProductFilter): Promise<Product[]>;
  save(product: Product): Promise<void>;
}

// Implementação real
class HttpProductRepository implements ProductRepository {
  constructor(private readonly baseUrl: string) {}

  async findById(id: string): Promise<Product | null> {
    const res = await fetch(`${this.baseUrl}/products/${id}`);
    return res.ok ? res.json() : null;
  }

  async findAll(filter: ProductFilter): Promise<Product[]> {
    const res = await fetch(`${this.baseUrl}/products?${new URLSearchParams(filter as any)}`);
    return res.json();
  }

  async save(product: Product): Promise<void> {
    await fetch(`${this.baseUrl}/products/${product.id}`, {
      method: 'PUT',
      body: JSON.stringify(product),
      headers: { 'Content-Type': 'application/json' },
    });
  }
}

// Decorator de cache (Map em memória)
class CachedProductRepository implements ProductRepository {
  private readonly cache = new Map<string, Product>();

  constructor(private readonly delegate: ProductRepository) {}

  async findById(id: string): Promise<Product | null> {
    if (this.cache.has(id)) return this.cache.get(id)!;
    const product = await this.delegate.findById(id);
    if (product) this.cache.set(id, product);
    return product;
  }

  async findAll(filter: ProductFilter): Promise<Product[]> {
    return this.delegate.findAll(filter);
  }

  async save(product: Product): Promise<void> {
    await this.delegate.save(product);
    this.cache.set(product.id, product);
  }
}

// Decorator de logging
class LoggedProductRepository implements ProductRepository {
  constructor(private readonly delegate: ProductRepository) {}

  async findById(id: string): Promise<Product | null> {
    console.log(`[repo] findById: ${id}`);
    const start = Date.now();
    const result = await this.delegate.findById(id);
    console.log(`[repo] findById: ${Date.now() - start}ms, found: ${!!result}`);
    return result;
  }

  async findAll(filter: ProductFilter): Promise<Product[]> {
    const result = await this.delegate.findAll(filter);
    console.log(`[repo] findAll: ${result.length} items`);
    return result;
  }

  async save(product: Product): Promise<void> {
    console.log(`[repo] save: ${product.id}`);
    return this.delegate.save(product);
  }
}

// Composição
const repo: ProductRepository = new LoggedProductRepository(
  new CachedProductRepository(
    new HttpProductRepository('https://api.example.com')
  )
);

Spring AOP — Decorator automático

O Spring AOP implementa o padrão Decorator automaticamente para cross-cutting concerns:

@Aspect
@Component
public class TimingAspect {
    @Around("@annotation(Timed)")
    public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return pjp.proceed(); // delega para o objeto real
        } finally {
            long elapsed = System.currentTimeMillis() - start;
            log.info("{} executou em {}ms", pjp.getSignature(), elapsed);
        }
    }
}

// Spring gera um proxy (Decorator) automaticamente para qualquer método @Timed
@Service
public class ProductService {
    @Timed
    public Product findById(String id) {
        return repository.findById(id).orElseThrow();
    }
}

O proxy gerado pelo Spring é exatamente um Decorator em runtime, sem que você escreva o código de wrapping manualmente.


No mundo real

java.io.InputStream e seus wrappers — o exemplo mais citado nos livros. BufferedInputStream, GZIPInputStream, CipherInputStream são todos decorators.

Spring Security filter chain — cada filtro de segurança (BasicAuthFilter, JwtAuthFilter, CsrfFilter) é um Decorator que envolve o próximo filtro na cadeia.

Spring AOP / @Transactional, @Cacheable, @Async — o Spring gera proxies (decorators) em runtime para adicionar transações, cache e execução assíncrona sem alterar o bean original.

Express.js / Koa middleware — cada middleware é um decorator que envolve o handler original, adicionando comportamento antes e depois.

React Higher-Order Components (HOC)withAuth(MyComponent) retorna um componente que envolve MyComponent com lógica de autenticação — decorator puro em componentes funcionais.


Quando usar

  • Adicionar comportamentos transversais (logging, cache, métricas, retry, autenticação) sem modificar a lógica central
  • Combinar comportamentos de forma flexível e dinâmica — escolher em runtime quais decorators aplicar
  • Quando herança resultaria em explosão combinatória de subclasses
  • Para seguir o Princípio de Responsabilidade Única: cada decorator tem uma única responsabilidade

Quando NÃO usar

  • Quando o comportamento adicional sempre se aplica — coloque direto na implementação
  • Quando a ordem dos decorators é crítica e não é óbvia — pode criar bugs sutis e dificultar depuração
  • Quando há apenas um comportamento a adicionar e ele não precisa ser opcional — composição simples é suficiente
  • Quando frameworks como Spring AOP já resolvem o problema automaticamente — implementar manualmente é redundante

Combinações com outros padrões

Decorator + Adapter: Decorator mantém a interface; Adapter muda a interface. Compostos:

PaymentGateway gw = new RetryDecorator(new StripeAdapter(stripeClient));

Decorator + Strategy: A Strategy decide o algoritmo; Decorator adiciona cross-cutting concerns em volta da Strategy:

DiscountStrategy strategy = new MeteredDiscount(new LoyaltyDiscount(), meterRegistry);

Decorator + Chain of Responsibility: Ambos passam adiante, mas CoR permite que um handler interrompa a cadeia. Decorator sempre delega ao próximo (exceto em casos de erro). Em middlewares HTTP, a linha entre os dois é tênue.