Padrão Estrutural (GoF)

Composite

Compõe objetos em estruturas de árvore para representar hierarquias parte-todo, permitindo que o cliente trate objetos individuais e composições de forma uniforme via uma interface comum.

Intenção

Compor objetos em árvores parte-todo e permitir que o código cliente trate um objeto individual (folha) e uma composição de objetos (nó composto) exatamente da mesma forma — por meio de uma interface comum.

Catalogado pelo GoF (1994) como padrão estrutural, o Composite é a resposta elegante a estruturas hierárquicas onde a lógica que se aplica a um elemento isolado deve se propagar recursivamente por toda a árvore. A operação tamanho() de um arquivo retorna seu tamanho fixo; a mesma operação em um diretório soma os tamanhos de todos os filhos — mas o código que chama tamanho() não precisa distinguir um do outro.

Problema

Imagine um sistema de arquivos. Ele tem arquivos — objetos simples com nome e tamanho fixo — e diretórios, que contêm outros arquivos e diretórios. Você quer calcular o tamanho total de qualquer item: seja um arquivo individual ou um diretório inteiro (somando recursivamente tudo que está dentro dele).

Sem o Composite, o código cliente precisa diferenciar os dois casos a todo momento:

  • Se é um arquivo, retorne seu tamanho diretamente.
  • Se é um diretório, itere sobre os filhos, verifique se cada filho é arquivo ou diretório, e recursão se necessário.

Isso resulta em condicionais espalhadas por todo o código cliente — e cada vez que você adiciona um novo tipo de nó (ex.: link simbólico), precisa atualizar todos esses pontos. O Composite elimina esse problema: folhas e compostos implementam a mesma interface; a recursão fica encapsulada dentro do composto.

Transparência vs Segurança

O GoF descreve duas abordagens para onde colocar os métodos de gerência de filhos (adicionar, remover):

  • Transparência (na interface Component): tanto folhas quanto compostos declaram os métodos de gerência. O cliente trata tudo uniformemente — mas folhas precisam implementar métodos que não fazem sentido para elas (geralmente lançando uma exceção ou não fazendo nada), o que viola o Princípio de Substituição de Liskov.
  • Segurança (só no Composite): apenas o composto expõe adicionar e remover. O cliente que precisar gerenciar filhos deve obter uma referência tipada como Diretorio, não como Componente. Perde-se alguma uniformidade, mas o código fica seguro e correto em tempo de compilação. Esta é a abordagem preferível em TypeScript e PHP.

Os exemplos abaixo adotam a abordagem de segurança: adicionar existe apenas em Diretorio (o Composite), não na interface Componente.

Solução

O Composite organiza o código em três participantes:

  1. Component (interface): o contrato compartilhado por folhas e compostos. Define as operações que o cliente pode chamar uniformemente. Ex.: interface Componente com nome(): string e tamanho(): number.
  2. Leaf (Folha): implementa Component e não tem filhos. Representa o objeto primitivo da hierarquia. Ex.: Arquivo — retorna seu tamanho diretamente.
  3. Composite (Composto): implementa Component e mantém uma lista de filhos do tipo Component. Implementa as operações recursando nos filhos. Ex.: Diretorio — soma o tamanho de todos os filhos, que podem ser outros diretórios ou arquivos.

A chave do padrão é que o Composite armazena referências ao tipo Componente (a interface), não ao tipo concreto. Isso permite misturar folhas e outros compostos na mesma lista de filhos, criando árvores de profundidade arbitrária.

Estrutura

        «interface» Componente
   ┌─────────────────────────────────┐
   │ + nome(): string                │
   │ + tamanho(): number             │
   └─────────────────────────────────┘
          ▲                   ▲
          │ implementa         │ implementa
       Arquivo              Diretorio
       (Folha)              (Composto)
  ┌──────────────────┐  ┌──────────────────────────────────┐
  │ - _nome: string  │  │ - filhos: Componente[]           │
  │ - _tamanho: num  │  │ + adicionar(c: Componente): this │
  │ + nome(): string │  │ + nome(): string                 │
  │ + tamanho(): num │  │ + tamanho(): number              │
  └──────────────────┘  │   → soma tamanhos dos filhos     │
                        └──────────────────────────────────┘
                                      │ filhos
                          ┌───────────┴────────────┐
                          ▼                         ▼
                       Arquivo                  Diretorio
                      (folha)              (pode conter mais filhos)


Árvore de exemplo:

  raiz/                        ← Diretorio
  ├── src/                     ← Diretorio
  │   ├── main.ts              ← Arquivo (1200 bytes)
  │   └── util.ts              ← Arquivo  (800 bytes)
  ├── dist/                    ← Diretorio
  │   └── main.js              ← Arquivo (3500 bytes)
  └── package.json             ← Arquivo  (400 bytes)

  raiz.tamanho()  == 5900   (soma recursiva de tudo)
  src.tamanho()   == 2000
  dist.tamanho()  == 3500

Exemplos de código

Exemplo 1 — Sistema de arquivos com cálculo recursivo de tamanho

O cliente chama tamanho() em qualquer Componente sem saber se está lidando com um arquivo ou um diretório inteiro. A recursão fica encapsulada dentro de Diretorio.

// ── Component — interface comum a folhas e compostos ─────────
interface Componente {
  nome(): string;
  tamanho(): number; // em bytes
}

// ── Leaf (Folha) — objeto sem filhos ──────────────────────────
class Arquivo implements Componente {
  constructor(
    private readonly _nome: string,
    private readonly _tamanho: number
  ) {}

  nome(): string    { return this._nome; }
  tamanho(): number { return this._tamanho; }
}

// ── Composite (Composto) — agrega e recursa nos filhos ────────
class Diretorio implements Componente {
  private readonly filhos: Componente[] = [];

  constructor(private readonly _nome: string) {}

  nome(): string { return this._nome; }

  adicionar(componente: Componente): this {
    this.filhos.push(componente);
    return this; // fluent — permite encadear chamadas
  }

  tamanho(): number {
    // Recursa nos filhos — não distingue Arquivo de Diretorio
    return this.filhos.reduce((acc, filho) => acc + filho.tamanho(), 0);
  }

  listar(nivel = 0): void {
    const p = "  ".repeat(nivel);
    console.log(`${p}[DIR]  ${this._nome}/ (${this.tamanho()} bytes)`);
    for (const filho of this.filhos) {
      if (filho instanceof Diretorio) {
        filho.listar(nivel + 1);
      } else {
        console.log(`${p}  [ARQ] ${filho.nome()} (${filho.tamanho()} bytes)`);
      }
    }
  }
}

// ── Código cliente — opera sobre Componente uniformemente ────
function exibirTamanho(componente: Componente): void {
  console.log(`"${componente.nome()}" ocupa ${componente.tamanho()} bytes`);
}

// ── Montagem da árvore ────────────────────────────────────────
const raiz = new Diretorio("raiz");
const src  = new Diretorio("src");
const dist = new Diretorio("dist");

src.adicionar(new Arquivo("main.ts", 1200))
   .adicionar(new Arquivo("util.ts", 800));

dist.adicionar(new Arquivo("main.js", 3500));

raiz.adicionar(src)
    .adicionar(dist)
    .adicionar(new Arquivo("package.json", 400));

raiz.listar();
// [DIR]  raiz/ (5900 bytes)
//   [DIR]  src/ (2000 bytes)
//     [ARQ] main.ts (1200 bytes)
//     [ARQ] util.ts (800 bytes)
//   [DIR]  dist/ (3500 bytes)
//     [ARQ] main.js (3500 bytes)
//   [ARQ] package.json (400 bytes)

// Mesma chamada — sem distinguir folha de composto:
exibirTamanho(new Arquivo("readme.md", 200));
// "readme.md" ocupa 200 bytes
exibirTamanho(raiz);
// "raiz" ocupa 5900 bytes

Exemplo 2 — Organograma com cálculo recursivo de folha de pagamento

O mesmo padrão se aplica a qualquer hierarquia parte-todo. Um Funcionario (folha) tem salário fixo; um Departamento (composto) soma os salários de todos os seus membros, que podem ser funcionários ou outros departamentos aninhados.

// ── Component ─────────────────────────────────────────────────
interface UnidadeOrg {
  titulo(): string;
  salarioTotal(): number; // em centavos
}

// ── Leaf ──────────────────────────────────────────────────────
class Funcionario implements UnidadeOrg {
  constructor(
    private readonly _titulo: string,
    private readonly _salario: number
  ) {}

  titulo(): string        { return this._titulo; }
  salarioTotal(): number  { return this._salario; }
}

// ── Composite ─────────────────────────────────────────────────
class Departamento implements UnidadeOrg {
  private readonly membros: UnidadeOrg[] = [];

  constructor(private readonly _titulo: string) {}

  titulo(): string { return this._titulo; }

  adicionar(membro: UnidadeOrg): this {
    this.membros.push(membro);
    return this;
  }

  salarioTotal(): number {
    return this.membros.reduce((acc, m) => acc + m.salarioTotal(), 0);
  }
}

// ── Uso ───────────────────────────────────────────────────────
const engenharia = new Departamento("Engenharia");
engenharia
  .adicionar(new Funcionario("Alice", 1200000))  // R$12.000
  .adicionar(new Funcionario("Bruno",  950000));  // R$ 9.500

const design = new Departamento("Design");
design.adicionar(new Funcionario("Carla", 800000)); // R$8.000

const empresa = new Departamento("Empresa");
empresa
  .adicionar(engenharia)
  .adicionar(design)
  .adicionar(new Funcionario("Diana (CEO)", 2500000)); // R$25.000

// O cliente opera sobre UnidadeOrg — não distingue folha de composto
const itens: UnidadeOrg[] = [engenharia, design, empresa];
for (const item of itens) {
  const valor = (item.salarioTotal() / 100).toFixed(2);
  console.log(`${item.titulo()}: R$${valor}`);
}
// Engenharia: R$21500.00   (12000 + 9500)
// Design:     R$8000.00
// Empresa:    R$54500.00   (21500 + 8000 + 25000 ✓)

Quando usar

  • Quando a estrutura é naturalmente hierárquica (parte-todo): sistemas de arquivos, menus de navegação, organogramas, árvores de expressão aritmética, componentes de UI aninhados. Se a estrutura é uma árvore, Composite é a escolha natural.
  • Quando o cliente deve tratar objetos simples e compostos uniformemente: se o código que percorre a hierarquia não deve se preocupar se o nó atual é folha ou composto, use Composite.
  • Quando operações precisam se propagar recursivamente: calcular totais, renderizar, serializar, aplicar permissões — qualquer operação que precisa visitar todos os nós da árvore.

Quando evitar

  • Quando a hierarquia é plana ou fixa: se os objetos não formam uma árvore real (ex.: uma lista de itens de mesmo nível), o overhead estrutural do Composite não se justifica.
  • Quando tipos diferentes requerem interfaces muito distintas: se folha e composto precisam de métodos muito diferentes, forçar uma interface comum inflada cria mais problemas do que resolve.
  • Quando performance é crítica em árvores muito profundas: a recursão do Composite percorre cada nó. Em árvores extremamente profundas, o custo de chamada pode ser relevante — considere caching de resultados intermediários ou percurso iterativo.

Prós e contras

Prós

  • Elimina condicionais espalhadas no cliente para distinguir folha de composto — a polimorfismo cuida disso.
  • Princípio Aberto/Fechado: novos tipos de folha ou composto podem ser adicionados sem alterar o código cliente ou os outros nós.
  • Permite construir estruturas de profundidade arbitrária com o mesmo conjunto de classes.
  • A operação se propaga naturalmente pela árvore — sem código de travessia no cliente.

Contras

  • Pode ser difícil restringir quais tipos de componentes podem ser filhos de quais compostos — a interface comum não expressa essas restrições.
  • A abordagem de transparência (métodos add/remove na interface Component) viola o Princípio de Substituição de Liskov quando aplicados a folhas.
  • Depurar árvores profundas pode ser trabalhoso — rastrear a origem de um valor recursivo exige percorrer toda a cadeia.

Armadilhas comuns

1. Interface inflada — métodos de gerência em folhas

A tentação de colocar adicionar() e remover() na interface Componente (abordagem de transparência) é real: o cliente ganha uniformidade total. O preço é que Arquivo precisa implementar esses métodos mesmo não tendo filhos. A prática idiomática em TypeScript e PHP é mantê-los apenas em Diretorio (abordagem de segurança). Quando o cliente precisar chamar adicionar(), ele já sabe que está lidando com um composto — o type system confirma isso em tempo de compilação.

2. Ciclos na árvore

Atenção: se um nó for adicionado como filho de si mesmo — ou como descendente de algum de seus próprios descendentes — a recursão de tamanho() entrará em loop infinito. O Composite não previne ciclos por padrão. Se o domínio permitir essa possibilidade (ex.: links simbólicos em sistemas de arquivos reais), implemente detecção de ciclos: mantenha um Set dos nós já visitados durante a recursão e abort se encontrar um repetido.

3. Confundir Composite com Decorator

Ambos usam composição recursiva e implementam a mesma interface do objeto que envolvem. A diferença está na intenção e na cardinalidade: o Decorator envolve exatamente um objeto e adiciona responsabilidade ao redor das suas chamadas. O Composite agrega N filhos e a sua operação primária é acumular ou propagar o resultado pelos filhos. Se você tem um "wrapper de um único objeto" que adiciona comportamento, é Decorator. Se você tem um nó que agrega e delega para múltiplos filhos, é Composite.

4. Caching ausente em árvores grandes

Cada chamada a tamanho() em um diretório raiz percorre toda a árvore. Se a árvore for grande e imutável (ou raramente muda), calcule e armazene o resultado em cache no composto, invalidando-o apenas quando um filho for adicionado ou removido. Sem esse cuidado, operações repetidas sobre a raiz têm custo O(n) a cada chamada.

Padrões relacionados

O Composite é frequentemente combinado ou comparado com outros padrões que também lidam com composição e travessia de estruturas:

O Decorator é o padrão com maior semelhança estrutural: ambos usam composição recursiva com uma interface comum. A diferença está na cardinalidade e na intenção — Decorator envolve um único objeto para adicionar responsabilidade; Composite agrega N filhos para propagar operações. O Proxy também controla acesso a um único objeto e pode ser aplicado a nós de uma árvore Composite para, por exemplo, carregá-los sob demanda (lazy loading de subárvores). O Visitor é o complemento natural do Composite: separa a lógica de travessia e processamento dos nós da estrutura da própria árvore, permitindo adicionar operações sem modificar as classes de nó. O Iterator pode ser usado para percorrer a árvore de forma uniforme sem expor sua estrutura interna.