EstruturalIntermediário6 min de leitura9 seções1994, Marco de referência: a ideia é mais velha que a data

Composite

Compõe objetos em árvores e faz o cliente tratar um item isolado e um grupo inteiro exatamente do mesmo jeito, através de uma interface comum.

Em 10 segundos

Folhas e grupos implementam a mesma interface, e o grupo delega aos filhos. Assim o cliente chama `tamanho()` na raiz sem saber se ali existe um arquivo ou dez mil — a recursão mora no padrão, não no chamador.

  • #arvore
  • #recursao
  • #hierarquia
  • #gof
Analogia

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.

01Conceito

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).

02Conceito

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.

Como se organiza
  • Pasta /composto — soma os filhos

    • leia.mdfolha — 120 bytes

    • Pasta fotoscomposto dentro de composto

      • a.jpgfolha — 2048 bytes

      • b.jpgfolha — 4096 bytes

A árvore é o padrão. Chamar tamanho() na raiz desce até as folhas e volta somando — e o cliente que fez a chamada não escreveu nenhuma recursão.
Passo a passoO fluxo, passo a passo
  1. 1

    Definir a interface

    Declare as operações de negócio que valem para folha e grupo (ex.: tamanho()).

  2. 2

    Implementar a folha

    O item indivisível faz o trabalho real — é onde a recursão termina.

  3. 3

    Implementar o composto

    Guarda filhos, delega a operação e combina os resultados (soma, concatena…).

  4. 4

    Chamar na raiz

    O cliente opera sobre a interface comum; a profundidade da árvore fica encapsulada.

03Anatomia

Navegue pelas camadas

Camada 1/3Componente

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.

04Estrutura

Estrutura

Diagrama de classes
05Implementação

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()); // 6264
06No mundo real

Casos 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.

08Cuidado

Armadilhas

  1. Folha que lança exceção

    Colocar adicionar() na interface comum para o cliente 'tratar tudo igual' e implementar na folha com throw 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.

  2. 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.

  3. 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.

09Decisão

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 filhos

O 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