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
adicionareremover. O cliente que precisar gerenciar filhos deve obter uma referência tipada comoDiretorio, não comoComponente. 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:
-
Component (interface): o contrato compartilhado por
folhas e compostos. Define as operações que o cliente pode chamar
uniformemente. Ex.: interface
Componentecomnome(): stringetamanho(): number. -
Leaf (Folha): implementa Component e não tem filhos.
Representa o objeto primitivo da hierarquia. Ex.:
Arquivo— retorna seu tamanho diretamente. -
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
<?php
// ── Component — interface comum a folhas e compostos ─────────
interface Componente
{
public function nome(): string;
public function tamanho(): int; // em bytes
}
// ── Leaf (Folha) — objeto sem filhos ──────────────────────────
class Arquivo implements Componente
{
public function __construct(
private readonly string $nome,
private readonly int $tamanho
) {}
public function nome(): string { return $this->nome; }
public function tamanho(): int { return $this->tamanho; }
}
// ── Composite (Composto) — agrega e recursa nos filhos ────────
class Diretorio implements Componente
{
/** @var Componente[] */
private array $filhos = [];
public function __construct(private readonly string $nome) {}
public function nome(): string { return $this->nome; }
public function adicionar(Componente $componente): static
{
$this->filhos[] = $componente;
return $this; // fluent — permite encadear chamadas
}
public function tamanho(): int
{
// Recursa nos filhos — não distingue Arquivo de Diretorio
return array_sum(array_map(
fn(Componente $filho) => $filho->tamanho(),
$this->filhos
));
}
public function listar(int $nivel = 0): void
{
$p = str_repeat(' ', $nivel);
echo "{$p}[DIR] {$this->nome}/ ({$this->tamanho()} bytes)" . PHP_EOL;
foreach ($this->filhos as $filho) {
if ($filho instanceof Diretorio) {
$filho->listar($nivel + 1);
} else {
echo "{$p} [ARQ] {$filho->nome()} ({$filho->tamanho()} bytes)" . PHP_EOL;
}
}
}
}
// ── Código cliente — opera sobre Componente uniformemente ────
function exibirTamanho(Componente $componente): void
{
echo "\"{$componente->nome()}\" ocupa {$componente->tamanho()} bytes" . PHP_EOL;
}
// ── Montagem da árvore ────────────────────────────────────────
$raiz = new Diretorio('raiz');
$src = new Diretorio('src');
$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)
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 ✓)
<?php
// ── Component ─────────────────────────────────────────────────
interface UnidadeOrg
{
public function titulo(): string;
public function salarioTotal(): int; // em centavos
}
// ── Leaf ──────────────────────────────────────────────────────
class Funcionario implements UnidadeOrg
{
public function __construct(
private readonly string $titulo,
private readonly int $salario
) {}
public function titulo(): string { return $this->titulo; }
public function salarioTotal(): int { return $this->salario; }
}
// ── Composite ─────────────────────────────────────────────────
class Departamento implements UnidadeOrg
{
/** @var UnidadeOrg[] */
private array $membros = [];
public function __construct(private readonly string $titulo) {}
public function titulo(): string { return $this->titulo; }
public function adicionar(UnidadeOrg $membro): static
{
$this->membros[] = $membro;
return $this;
}
public function salarioTotal(): int
{
return array_sum(array_map(
fn(UnidadeOrg $m) => $m->salarioTotal(),
$this->membros
));
}
}
// ── Uso ───────────────────────────────────────────────────────
$engenharia = new Departamento('Engenharia');
$engenharia
->adicionar(new Funcionario('Alice', 1200000)) // R$12.000
->adicionar(new Funcionario('Bruno', 950000)); // R$ 9.500
$design = new Departamento('Design');
$design->adicionar(new Funcionario('Carla', 800000)); // R$8.000
$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
$itens = [$engenharia, $design, $empresa];
foreach ($itens as $item) {
printf("%s: R$%.2f\n", $item->titulo(), $item->salarioTotal() / 100);
}
// 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.