Pular para o conteúdo

O Composite é um padrão de projeto estrutural que permite organizar objetos em uma estrutura de árvore e tratar tanto elementos simples quanto grupos de elementos por meio da mesma interface. Em vez de o cliente precisar diferenciar manualmente um objeto isolado de um conjunto inteiro, ele conversa com todos os elementos da mesma forma.

Esse padrão aparece quando o domínio possui composições naturais, como pastas e arquivos, categorias e subcategorias, menus e submenus, ou caixas que contêm produtos e outras caixas. O Composite existe para simplificar o código cliente e para permitir operações recursivas sobre estruturas hierárquicas.

Imagine um sistema de e-commerce que precisa calcular o preço total de um pedido. Esse pedido pode conter:

  • produtos avulsos
  • kits promocionais
  • caixas com vários itens
  • caixas que contêm outras caixas

Sem um modelo adequado, o cliente tende a criar verificações como:

  • se for produto, some o preço direto
  • se for caixa, percorra os itens internos
  • se houver outra caixa, repita a lógica novamente
  • se houver novo tipo de agrupamento, altere o cliente outra vez

Esse desenho gera problemas práticos:

  • o código cliente fica cheio de condicionais
  • novas estruturas hierárquicas exigem mudanças em vários pontos
  • a recursão fica espalhada pelo sistema
  • o acoplamento com classes concretas aumenta
  • a manutenção se torna mais difícil com o crescimento da árvore

Em resumo, o problema não é apenas calcular algo. O problema é tratar uniformemente objetos simples e compostos em uma estrutura hierárquica.

O Composite resolve isso definindo uma abstração comum para todos os elementos da árvore.

  1. Crie uma interface ou classe base chamada, por exemplo, ComponentePedido.
  2. Faça objetos simples, como Produto, implementarem essa abstração.
  3. Faça objetos compostos, como Caixa ou Kit, implementarem a mesma abstração.
  4. No objeto composto, mantenha uma coleção de filhos do tipo da abstração comum.
  5. Delegue a operação para os filhos e agregue o resultado.

Assim, quando o cliente pedir calcularPreco(), tanto uma folha quanto um grupo saberão responder. Uma folha devolve seu próprio valor. Um composite percorre seus filhos, soma os resultados e retorna o total.

O ponto central do padrão é este: o cliente não precisa saber se está lidando com um item único ou com um grupo inteiro.

Pense na estrutura de pastas do sistema operacional. Um arquivo é um elemento simples. Uma pasta é um elemento composto, porque pode conter arquivos e outras pastas.

Quando você pede o tamanho total de uma pasta, o sistema percorre tudo o que está dentro dela, incluindo subpastas. Para quem usa, a interação continua natural: tanto um arquivo quanto uma pasta são elementos do sistema de arquivos, mas uma pasta agrega outros elementos internamente.

Use Composite quando:

  • o domínio puder ser representado como árvore
  • houver elementos simples e agrupamentos aninhados
  • o cliente precisar tratar itens e grupos de forma uniforme
  • operações recursivas forem comuns, como somar, renderizar ou percorrer
  • a estrutura puder crescer com vários níveis de composição

Exemplos reais em software moderno:

  • menus com submenus em interfaces web
  • pastas e arquivos em gerenciadores de documentos
  • componentes visuais com containers e widgets filhos
  • categorias e subcategorias em marketplaces
  • comentários com respostas encadeadas em plataformas online

Composite pode ser mal utilizado quando a equipe tenta forçar uma hierarquia onde ela não existe de verdade. Alguns erros frequentes são:

  • criar composites sem haver necessidade de composição recursiva
  • colocar métodos demais na interface comum, tornando-a confusa para folhas
  • usar o padrão em estruturas pequenas e estáveis demais
  • misturar responsabilidades de navegação, negócio e persistência no mesmo composite

As consequências incluem excesso de classes, dificuldade para entender a interface base e objetos folha precisando implementar operações que fazem pouco sentido.

Composite é valioso quando a árvore faz parte do problema. Se a estrutura não for hierárquica, o padrão pode adicionar mais complexidade do que benefício.

Uma forma incremental de implementar Composite é:

  1. Identifique qual elemento comum existe em toda a estrutura.
  2. Defina uma interface com a operação principal, como calcularPreco() ou renderizar().
  3. Crie uma ou mais classes folha para os elementos simples.
  4. Crie uma classe composite para os elementos que contêm filhos.
  5. Armazene os filhos usando a abstração comum.
  6. No composite, percorra os filhos e combine os resultados.
  7. Exponha métodos de adicionar e remover quando fizer sentido.

Antes de escrever o código, vale responder: o cliente realmente precisa ignorar a diferença entre folha e composite? Se sim, o padrão costuma ser uma boa escolha.

No exemplo abaixo, um sistema de loja calcula o total de itens avulsos e caixas compostas de forma uniforme.

interface ItemPedido {
double calcularPreco();
}
class Produto implements ItemPedido {
private String nome;
private double preco;
public Produto(String nome, double preco) {
this.nome = nome;
this.preco = preco;
}
@Override
public double calcularPreco() {
return preco;
}
}
class Caixa implements ItemPedido {
private List<ItemPedido> itens = new ArrayList<>();
public void adicionar(ItemPedido item) {
itens.add(item);
}
@Override
public double calcularPreco() {
double total = 0;
for (ItemPedido item : itens) {
total += item.calcularPreco();
}
return total;
}
}

Nesse código, Produto é a folha e Caixa é o composite. Ambos implementam ItemPedido, então o cliente pode chamar calcularPreco() sem conhecer o tipo concreto do objeto.

Essa uniformidade reduz condicionais e facilita a extensão da estrutura com novos tipos de folha ou novos composites.

  • simplifica o tratamento de estruturas hierárquicas
  • reduz condicionais no código cliente
  • favorece polimorfismo e recursão
  • facilita a extensão com novos tipos de componentes
  • melhora a legibilidade quando o domínio é naturalmente em árvore
  • pode tornar a interface base genérica demais
  • aumenta a quantidade de classes no sistema
  • pode dificultar validações específicas de folhas e composites
  • nem toda estrutura com coleções justifica o uso do padrão

O Composite se relaciona com outros padrões importantes:

  • Decorator: ambos usam composição recursiva, mas Decorator encapsula um único componente e adiciona comportamento; Composite organiza vários filhos em árvore
  • Visitor: pode ser usado para executar operações em toda a árvore sem colocar toda a lógica dentro dos componentes
  • Iterator: ajuda a percorrer estruturas Composite de forma controlada
  • Builder: pode ser usado para montar árvores Composite complexas passo a passo
  • Flyweight: pode ajudar quando muitas folhas compartilham dados e o consumo de memória se torna relevante

Essas relações mostram que Composite não serve apenas para agrupamento. Ele também abre espaço para outras estratégias de navegação, construção e extensão da árvore.

As formas mais comuns de aplicar Composite são:

  • interface comum mínima com operações seguras para folhas e composites
  • métodos de add e remove apenas no composite
  • métodos de gerenciamento de filhos na interface base, quando a uniformidade do cliente for mais importante
  • composites especializados para regras distintas, como Menu, Secao, Pacote ou Categoria

Em Java, uma abordagem comum é usar uma interface para a operação principal e deixar apenas o composite expor métodos de montagem da árvore.

Agora veja um caso mais realista em um sistema acadêmico. A plataforma virtual permite montar módulos de curso com aulas avulsas e unidades que agrupam outras partes do conteúdo.

import java.util.ArrayList;
import java.util.List;
interface ComponenteCurso {
int calcularCargaHoraria();
void exibir(String prefixo);
}
class Aula implements ComponenteCurso {
private String titulo;
private int horas;
public Aula(String titulo, int horas) {
this.titulo = titulo;
this.horas = horas;
}
@Override
public int calcularCargaHoraria() {
return horas;
}
@Override
public void exibir(String prefixo) {
System.out.println(prefixo + "Aula: " + titulo + " (" + horas + "h)");
}
}
class Unidade implements ComponenteCurso {
private String nome;
private List<ComponenteCurso> componentes = new ArrayList<>();
public Unidade(String nome) {
this.nome = nome;
}
public void adicionar(ComponenteCurso componente) {
componentes.add(componente);
}
@Override
public int calcularCargaHoraria() {
int total = 0;
for (ComponenteCurso componente : componentes) {
total += componente.calcularCargaHoraria();
}
return total;
}
@Override
public void exibir(String prefixo) {
System.out.println(prefixo + "Unidade: " + nome);
for (ComponenteCurso componente : componentes) {
componente.exibir(prefixo + " ");
}
}
}
public class PlataformaCursos {
public static void main(String[] args) {
Aula aula1 = new Aula("Introdução ao Composite", 2);
Aula aula2 = new Aula("Exemplo prático", 1);
Aula aula3 = new Aula("Exercícios", 1);
Unidade modulo1 = new Unidade("Fundamentos");
modulo1.adicionar(aula1);
modulo1.adicionar(aula2);
Unidade curso = new Unidade("Padrões Estruturais");
curso.adicionar(modulo1);
curso.adicionar(aula3);
curso.exibir("");
System.out.println("Carga horária total: " + curso.calcularCargaHoraria() + "h");
}
}

Nesse exemplo, Aula é uma folha e Unidade é um composite. O cliente monta uma árvore de conteúdos e consegue calcular a carga horária total ou exibir a estrutura completa sem diferenciar cada nível manualmente.

Esse tipo de modelagem funciona bem quando o domínio possui partes e subpartes que podem ser tratadas como um todo coerente.