Introdução
Seção intitulada “Introdução”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.
Problema
Seção intitulada “Problema”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
instanceofpara distinguirArquivodePasta - 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.
Solução
Seção intitulada “Solução”O Composite resolve isso criando uma interface comum para folhas e containers:
- Defina uma interface (ou classe abstrata)
Component, com as operações comuns. - Crie a folha (
Leaf): o nó sem filhos, que implementa as operações diretamente. - Crie o composto (
Composite): o nó que guarda filhos do tipoComponente delega o trabalho a eles. - 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.
Analogia
Seção intitulada “Analogia”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.
Aplicabilidade
Seção intitulada “Aplicabilidade”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.awtComponent/Container) - estruturas organizacionais, catálogos e combos aninhados
Anti-padrão / Mau uso
Seção intitulada “Anti-padrão / Mau uso”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.
Como implementar
Seção intitulada “Como implementar”Uma forma incremental de implementar Composite é:
- Identifique a operação que faz sentido para todos os nós (ex.:
obterTamanho,getPreco). - Defina a interface
Componentcom essa operação. - Implemente a folha, que resolve a operação sozinha.
- Implemente o composto, que guarda os filhos e delega/combina os resultados.
- 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.
Exemplo em código
Seção intitulada “Exemplo em código”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”
Contras
Seção intitulada “Contras”- 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
Relações com outros padrões/conceitos
Seção intitulada “Relações com outros padrões/conceitos”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).
Implementações alternativas (quando aplicável)
Seção intitulada “Implementações alternativas (quando aplicável)”As formas mais comuns de implementar Composite são:
- transparente: os métodos de filhos (
adicionar/remover) ficam na interfaceComponent— 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.
Exemplo completo
Seção intitulada “Exemplo completo”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.
