Pular para o conteúdo

O Composite é um padrão de projeto estrutural que permite compor objetos em estruturas de árvore para representar hierarquias do tipo parte-todo. A ideia central é que o código cliente trate objetos individuais e composições de objetos de maneira uniforme, usando a mesma interface.

Ele aparece sempre que um problema tem uma estrutura recursiva: uma pasta que contém arquivos e outras pastas, um menu com itens e submenus, um elemento de interface que contém outros elementos. Em todos esses casos existe uma “coisa” que pode conter outras coisas do mesmo tipo.

Imagine um sistema de backup que precisa percorrer o conteúdo de um sistema de arquivos. Pastas podem conter arquivos ou outras pastas.

Sem um modelo comum, cada tipo de nó é tratado de um jeito diferente, e o código que percorre a árvore precisa saber quem é quem:

  • o cliente usa instanceof para distinguir Arquivo de Pasta
  • a recursão de percorrimento é reescrita em cada ponto do sistema
  • as pastas guardam os filhos como List<Object>, sem segurança de tipos
  • adicionar um novo tipo de nó exige revisar todos os pontos que percorrem a árvore

O problema cresce porque a lógica da estrutura (como somar tamanhos, como listar, como buscar) fica fora dos objetos, espalhada pelo cliente.

O Composite resolve isso criando uma interface comum para folhas e containers:

  1. Defina uma interface (ou classe abstrata) Component, com as operações comuns.
  2. Crie a folha (Leaf): o nó sem filhos, que implementa as operações diretamente.
  3. Crie o composto (Composite): o nó que guarda filhos do tipo Component e delega o trabalho a eles.
  4. Faça o cliente falar apenas com Component.

Como o composto guarda filhos da própria interface, ele pode conter folhas e outros compostos — formando uma árvore recursiva. A recursão passa a morar dentro das classes, e o cliente chama sempre o mesmo método.

Pense no organograma de uma empresa. Um departamento contém funcionários ou sub-departamentos. Para saber “quantas pessoas há abaixo de um diretor”, você soma a árvore recursivamente: cada departamento pergunta aos seus sub-departamentos.

Do ponto de vista de quem pergunta, chefe e subordinado são todos “funcionários”. O mesmo acontece no Composite: para o cliente, tudo é um Component.

Use Composite quando:

  • há uma estrutura recursiva parte-todo (itens dentro de itens)
  • o cliente deve tratar item e grupo de itens da mesma forma
  • você quer processar a estrutura de forma genérica e recursiva
  • deseja adicionar novos tipos de nó sem alterar o código que percorre a árvore

Exemplos reais em software moderno:

  • sistemas de arquivos (arquivos e pastas)
  • menus com submenus e itens
  • DOM de páginas web (elementos contêm elementos)
  • interfaces gráficas (java.awt Component/Container)
  • estruturas organizacionais, catálogos e combos aninhados

Composite pode ser mal utilizado quando a equipe o aplica sem haver uma hierarquia real. Os erros mais comuns são:

  • criar uma interface única para objetos que não compartilham operações relevantes
  • aplicar o padrão em listas planas, sem aninhamento ou recursão
  • forçar uma interface comum “inflada”, cheia de métodos que a folha ignora
  • usar herança genérica demais só para “ficar elegante”

As consequências costumam ser uma interface poluída, métodos que lançam exceção ou não fazem nada nas folhas, e um design mais difícil de explicar do que o problema original.

Composite funciona bem quando a estrutura é realmente recursiva e o tratamento uniforme traz ganho real.

Uma forma incremental de implementar Composite é:

  1. Identifique a operação que faz sentido para todos os nós (ex.: obterTamanho, getPreco).
  2. Defina a interface Component com essa operação.
  3. Implemente a folha, que resolve a operação sozinha.
  4. Implemente o composto, que guarda os filhos e delega/combina os resultados.
  5. Decida onde ficam os métodos de filhos (adicionar/remover): na interface (transparente) ou só no composto (seguro).

Antes de começar, vale perguntar: essa estrutura é realmente recursiva? O tratamento uniforme simplifica o cliente? Se sim, Composite é um forte candidato.

No exemplo abaixo, um sistema de arquivos trata arquivos e pastas de forma uniforme.

interface Component {
String getNome();
int obterTamanho();
}
class Arquivo implements Component {
private final String nome;
private final int tamanho;
public Arquivo(String nome, int tamanho) {
this.nome = nome;
this.tamanho = tamanho;
}
@Override
public String getNome() { return nome; }
@Override
public int obterTamanho() { return tamanho; }
}
class Pasta implements Component {
private final String nome;
private final List<Component> filhos = new ArrayList<>();
public Pasta(String nome) { this.nome = nome; }
public void adicionar(Component filho) { filhos.add(filho); }
@Override
public String getNome() { return nome; }
@Override
public int obterTamanho() {
int total = 0;
for (Component filho : filhos) {
total += filho.obterTamanho(); // recursão
}
return total;
}
}

Nesse código, Arquivo é a folha e Pasta é o composto. O obterTamanho() da pasta soma recursivamente o tamanho de todos os filhos — sejam arquivos ou outras pastas.

  • o cliente trata folha e composição da mesma forma
  • a recursão fica natural, dentro das classes
  • novos tipos de nó entram sem alterar o código que percorre a árvore (OCP)
  • modela fielmente estruturas “parte-todo”
  • a interface comum pode ficar genérica demais
  • é difícil restringir o que cada nó aceita como filho
  • pode ser overengineering se a estrutura não for recursiva de verdade

O Composite se relaciona com outros padrões importantes:

  • Decorator: ambos são estruturais e usam composição. Composite agrega vários filhos formando uma árvore; Decorator envolve um objeto formando uma cadeia de responsabilidades. O Decorator pode ser visto como um Composite “degenerado”, com um único filho.
  • Visitor: frequentemente usado para percorrer a árvore do Composite sem colocar a operação dentro dos nós.
  • Iterator: útil para percorrer os filhos de um composto de forma uniforme.
  • Flyweight: pode ser combinado com Composite para compartilhar folhas repetidas (ex.: folhas em comum).

As formas mais comuns de implementar Composite são:

  • transparente: os métodos de filhos (adicionar/remover) ficam na interface Component — o cliente trata tudo igual, mas a folha herda métodos que “não fazem sentido”
  • segura: os métodos de filhos existem somente no composto — mais segura, mas o cliente precisa distinguir os tipos para montar a árvore

Em Java, é comum usar a versão transparente, com a interface comum e o composto implementando a recursão.

Agora veja um caso mais realista: o cálculo de um combo de lanchonete, em que o ComboFamiliar pode conter um ComboDuplo dentro dele.

interface ItemMenu {
String getNome();
double getPreco();
}
class Prato implements ItemMenu {
private final String nome;
private final double preco;
public Prato(String nome, double preco) {
this.nome = nome;
this.preco = preco;
}
@Override
public String getNome() { return nome; }
@Override
public double getPreco() { return preco; }
}
class Combo implements ItemMenu {
private final String nome;
private final List<ItemMenu> itens = new ArrayList<>();
public Combo(String nome) { this.nome = nome; }
public void adicionar(ItemMenu item) { itens.add(item); }
@Override
public String getNome() { return nome; }
@Override
public double getPreco() {
double total = 0;
for (ItemMenu item : itens) {
total += item.getPreco();
}
return total;
}
}
public class Restaurante {
public static void main(String[] args) {
Prato hamburguer = new Prato("Hambúrguer", 15.0);
Prato batata = new Prato("Batata Frita", 8.0);
Prato refrigerante = new Prato("Refrigerante", 6.0);
Combo duplo = new Combo("Combo Duplo");
duplo.adicionar(hamburguer);
duplo.adicionar(hamburguer);
Combo familia = new Combo("Combo Família");
familia.adicionar(duplo); // sub-combo dentro do combo
familia.adicionar(batata);
familia.adicionar(refrigerante);
System.out.println(duplo.getNome() + ": " + duplo.getPreco());
System.out.println(familia.getNome() + ": " + familia.getPreco());
}
}

Nesse exemplo, Prato é a folha e Combo é o composto. O preço do ComboFamiliar já inclui o preço do ComboDuplo, porque o getPreco() soma recursivamente os filhos. Adicionar um novo tipo de prato não exige mudar o Combo nem a Main.