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.