A caixa de mudança
Ao calcular o peso de uma mudança, você não pergunta se cada volume é 'um objeto' ou 'uma caixa com objetos dentro' — você pesa. Uma caixa pesa a soma do que carrega, e dentro dela pode haver outras caixas. A balança tem uma operação só, e funciona igual para um livro solto e para a caixa que contém a estante inteira.
O problema
Hierarquias parte-todo aparecem em todo canto: pastas com pastas, menus com submenus, kits de produto que contêm outros kits. O que muda é a profundidade — e ela raramente é conhecida de antemão.
Sem uma interface comum, cada operação (calcular tamanho, renderizar, aplicar desconto) precisa perguntar 'isto é um item ou um grupo?' antes de decidir o que fazer.
Essa ramificação é pior do que parece: ela não fica num lugar só. Cada operação nova repete o if, e cada tipo novo de nó exige revisitar todos os if existentes. É o cheiro clássico que o polimorfismo resolve.
O Composite troca a ramificação por despacho dinâmico: a decisão 'sou folha ou grupo?' já está codificada na classe do objeto. Chamar o método certo é trabalho da linguagem, não seu.
O padrão tem um parentesco próximo com Decorator (ambos compõem objetos da mesma interface), mas a intenção difere: o Decorator embrulha UM objeto para acrescentar comportamento; o Composite agrupa N objetos para representar uma hierarquia. E costuma andar junto com Iterator (para percorrer a árvore) e Visitor (para adicionar operações sem tocar nos nós).
Transparência × segurança
A decisão mais controversa do padrão: os métodos de gerenciar filhos (adicionar, remover) ficam na interface comum ou só no composto?
Na versão transparente, adicionar mora na interface. O cliente trata todo mundo igual de verdade — mas uma folha precisa implementar adicionar, e a única saída honesta é lançar exceção. Isso viola Liskov: o subtipo não cumpre o contrato do supertipo.
Na versão segura, adicionar existe só no composto. O tipo passa a ser verdadeiro, mas o cliente precisa de um cast (ou de um type guard) para montar a árvore — a uniformidade quebra justamente na construção.
A escolha honesta depende de onde a árvore é montada. Se a construção acontece num lugar controlado (um parser, um builder, um repositório) e o resto do sistema só lê, a versão segura é melhor: o código que monta já conhece os tipos concretos. Se clientes arbitrários precisam recompor a árvore, a transparente reduz atrito ao custo de um método que mente.
Pasta /composto — soma os filhos
leia.mdfolha — 120 bytes
Pasta fotoscomposto dentro de composto
a.jpgfolha — 2048 bytes
b.jpgfolha — 4096 bytes
- 1
Definir a interface
Declare as operações de negócio que valem para folha e grupo (ex.: tamanho()).
- 2
Implementar a folha
O item indivisível faz o trabalho real — é onde a recursão termina.
- 3
Implementar o composto
Guarda filhos, delega a operação e combina os resultados (soma, concatena…).
- 4
Chamar na raiz
O cliente opera sobre a interface comum; a profundidade da árvore fica encapsulada.
Navegue pelas camadas
Camada 1/3Componente
use ← →Camada 1
Componente
a interface que apaga a diferença
Declara as operações de negócio que valem tanto para um item quanto para um grupo. Quanto mais enxuta, mais fácil é honrá-la nos dois lados — cada método adicionado precisa fazer sentido para uma folha.
No código
interface No { tamanho(): number }Se violar: se a interface cresce com métodos que só o composto sabe cumprir, a folha passa a lançar exceção e o polimorfismo vira mentira.
Estrutura
Código
interface No {
tamanho(): number;
}
// Folha: o caso base, indivisivel
class Arquivo implements No {
constructor(private nome: string, private bytes: number) {}
tamanho(): number {
return this.bytes;
}
}
// Composto: contem outros nos (folhas OU compostos) e delega
class Pasta implements No {
private filhos: No[] = [];
constructor(private nome: string) {}
adicionar(no: No): this {
this.filhos.push(no);
return this;
}
tamanho(): number {
// a recursao acontece aqui — e o cliente nunca a ve
return this.filhos.reduce((total, f) => total + f.tamanho(), 0);
}
}
const raiz = new Pasta("/")
.adicionar(new Arquivo("leia.md", 120))
.adicionar(
new Pasta("fotos")
.adicionar(new Arquivo("a.jpg", 2048))
.adicionar(new Arquivo("b.jpg", 4096)),
);
// Uma chamada so, sem saber a profundidade da arvore
console.log(raiz.tamanho()); // 6264from abc import ABC, abstractmethod
class No(ABC):
@abstractmethod
def tamanho(self) -> int: ...
# Folha: o caso base, indivisivel
class Arquivo(No):
def __init__(self, nome, bytes_):
self._nome, self._bytes = nome, bytes_
def tamanho(self):
return self._bytes
# Composto: contem outros nos e delega
class Pasta(No):
def __init__(self, nome):
self._nome = nome
self._filhos = []
def adicionar(self, no):
self._filhos.append(no)
return self
def tamanho(self):
# a recursao mora aqui — o cliente nunca a ve
return sum(f.tamanho() for f in self._filhos)
raiz = Pasta("/")
raiz.adicionar(Arquivo("leia.md", 120))
raiz.adicionar(
Pasta("fotos").adicionar(Arquivo("a.jpg", 2048)).adicionar(Arquivo("b.jpg", 4096))
)
print(raiz.tamanho()) # 6264Casos de uso
Árvore de componentes de UI
Cenário
Uma interface é feita de elementos que contêm outros elementos: um formulário tem seções, que têm campos, que podem ter ícones — e a profundidade depende da tela.
Como o padrão entra
Todo elemento implementa render(). Containers renderizam os filhos e compõem o resultado. É exatamente o modelo do DOM e do React: um componente devolve outros componentes, e a raiz não sabe quantos níveis existem abaixo.
Trade-off: Operações que precisam de contexto de cima (tema, largura disponível) não fluem naturalmente pela recursão — daí nascerem mecanismos extras como context/provider para furar a hierarquia.
Permissões em organograma
Cenário
Um gestor pode ver os dados da própria equipe, incluindo as equipes dos gestores abaixo dele, com profundidade variável conforme a área.
Como o padrão entra
Cada nó (pessoa ou equipe) implementa colaboradores(). Uma equipe devolve os próprios membros mais os das subequipes; uma pessoa devolve a si mesma. A consulta 'quem eu posso ver' vira uma chamada na raiz da subárvore do gestor.
Trade-off: Em organogramas grandes, a recursão vira muitas idas ao banco. Na prática se troca a árvore em memória por consultas com CTE recursiva ou por colunas de caminho materializado — o padrão continua no modelo, mas some da camada de dados.
Kits e combos em e-commerce
Cenário
Um 'kit gamer' contém teclado, mouse e headset; um deles pode ser outro kit ('combo periféricos'), e o preço e o peso precisam sair certos para o frete.
Como o padrão entra
Produto e Kit implementam preco() e peso(). O kit soma os filhos e aplica seu desconto. Adicionar um kit dentro de outro não exige código novo.
Trade-off: Descontos aninhados compõem de formas surpreendentes (desconto sobre desconto) e estoque de kit depende do estoque de todos os filhos — regras que a recursão simples não expressa e que acabam num serviço à parte.
A folha que mente no contrato
Compila, roda, passa no code review. E quebra depois.
Para 'tratar tudo igual', adicionar entra na interface comum. A folha implementa com throw. O polimorfismo vira armadilha: o tipo promete o que a folha não cumpre.
interface No {
tamanho(): number;
adicionar(no: No): void; // na interface "transparente"
}
class Arquivo implements No {
constructor(private bytes: number) {}
tamanho() { return this.bytes; }
adicionar(_no: No) {
throw new Error("arquivo nao tem filhos"); // <- Liskov quebrado
}
}
class Pasta implements No {
private filhos: No[] = [];
adicionar(no: No) { this.filhos.push(no); }
tamanho() {
return this.filhos.reduce((t, f) => t + f.tamanho(), 0);
}
}
// O cliente "trata tudo igual"... ate chamar adicionar numa folha.
function montar(raiz: No, filho: No) {
raiz.adicionar(filho); // explode se raiz for Arquivo
}O que quebra
- Na montagemUm cast errado ou um nó trocado explode longe de onde a árvore foi construída.
- No testeTestar o cliente exige garantir que ninguém chama adicionar em folha — o contrato não protege.
- Na revisãoUnsupportedOperation na folha é cheiro clássico de Liskov violado de propósito.
A saída: Versão segura: adicionar só no composto; quem monta a árvore já conhece o tipo. Ou aceite que a folha trate filhos de forma sensata — nunca com throw como contrato.
Armadilhas
Folha que lança exceção
Colocar
adicionar()na interface comum para o cliente 'tratar tudo igual' e implementar na folha comthrow new UnsupportedOperationé violar Liskov de forma explícita: o código que recebe um Componente não pode confiar no contrato. Ou o método sai da interface, ou a folha aceita a operação de forma sensata.Ciclos na árvore
Nada no padrão impede alguém de adicionar um nó como filho de seu próprio descendente. Quando isso acontece, a primeira chamada recursiva vira stack overflow — em produção, longe de onde o ciclo foi criado. Se a árvore é montada por entrada externa, validar aciclicidade na inserção não é preciosismo.
Recursão cara escondida
A beleza do padrão é esconder a recursão; o risco é esconder o custo junto.
raiz.tamanho()parece uma chamada barata e pode percorrer milhões de nós ou disparar uma query por nó. Em árvores grandes, memoizar o resultado no composto ou materializar o agregado deixa de ser otimização e vira requisito.
Quando usar × quando evitar
Use quando
3- Os dados formam uma árvore parte-todo com profundidade variável.
- A mesma operação precisa valer para um item e para um grupo, sem o cliente distinguir.
- Você quer que adicionar um tipo novo de nó não exija revisitar os chamadores.
Evite quando
3- A estrutura é plana ou de profundidade fixa — uma lista basta.
- Folha e composto têm contratos genuinamente diferentes.
- A travessia é cara e precisa de controle fino (paginação, corte por profundidade) que a recursão esconde.
Testar o que ficou
3 armadilhas de Composite viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
A árvore do DOMUm<div>e um nó de texto respondem à mesma interface de nó; o container se trata como a folha.children do JSXUm componente recebe um filho ou cem sem mudar de código, porque a composição é uniforme.Sistema de arquivosPasta e arquivo compartilham operações; copiar uma pasta é copiar recursivamente os filhos.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Folha e grupo compartilham a mesma interface.
pasta.adicionar(arquivo);
pasta.tamanho(); // soma os filhosO que ele cobra
1 salto a mais entre a chamada e o efeito.
- Folha e composto passam a compartilhar uma interface que nem sempre encaixa nos dois
- Operações que só fazem sentido no composto vazam para a folha, ou viram no-op estranho
Não vale se: a estrutura não é recursiva de verdade — forçar árvore onde há só uma lista plana complica sem ganho.
Costuma ser confundido com
Onde isto aparece
Continue por aqui
- DecoratorAdiciona comportamento a um objeto em tempo de execução envolvendo-o em camadas empilháveis que compartilham a mesma interface — composição no lugar de herança.
- IteratorOferece uma forma de percorrer os elementos de uma coleção sequencialmente sem expor como ela é construída por dentro — e permite mais de uma travessia simultânea.
- VisitorSepara operações da estrutura de objetos sobre a qual elas atuam, permitindo adicionar comportamentos novos sem modificar as classes existentes — ao custo de congelar o conjunto de tipos.