Padrão Comportamental (GoF)

Iterator

Fornece uma forma de acessar sequencialmente os elementos de uma coleção sem expor sua representação interna — desacoplando o algoritmo de percorrimento da estrutura de dados subjacente.

Intenção

Encapsular o mecanismo de travessia de uma coleção num objeto separado (o Iterator), expondo apenas uma interface de acesso sequencial — temProximo() / proximo() no estilo GoF, ou os protocolos nativos das linguagens (Symbol.iterator e generators em TypeScript; Iterator/IteratorAggregate e generators em PHP). O cliente percorre a coleção sem saber se ela é um array, uma lista encadeada, uma árvore ou um resultado de banco de dados.

Catalogado pelo GoF (1994) como padrão comportamental, o Iterator é hoje tão fundamental que todas as linguagens modernas o incorporaram nos seus protocolos de iteração nativos. Em TypeScript, qualquer objeto que implemente Symbol.iterator pode ser usado em for...of, spread ([...col]) e desestruturação. Em PHP, implementar Iterator ou IteratorAggregate habilita o foreach nativo.

Problema

Uma aplicação de músicas armazena a playlist numa lista encadeada customizada. A interface de exibição precisa percorrer a lista para mostrar as faixas. A abordagem direta expõe a estrutura interna:

// Abordagem ingênua — NÃO faça isso:
class PlaylistUI {
  mostrar(playlist: ListaEncadeada): void {
    // Acoplamento direto à estrutura interna — acessa o nó diretamente.
    let no = playlist.cabeca;   // propriedade interna exposta
    while (no !== null) {
      console.log(no.valor);    // acessa o campo interno do nó
      no = no.proximo;          // navega pela estrutura interna
    }
  }
}

Se a ListaEncadeada mudar para um array circular ou uma árvore balanceada, toda a lógica de percorrimento na UI precisa ser reescrita. Além disso, implementar uma segunda forma de travessia (ex.: em ordem reversa) exige duplicar o loop em outro lugar — sem nenhuma abstração reutilizável.

O Iterator extrai o percorrimento para um objeto dedicado. A coleção expõe apenas um método criarIterador() — o cliente usa a interface do Iterator sem saber nada sobre a estrutura interna da coleção.

Solução

O Iterator organiza o código em quatro participantes:

  1. Iterator (interface): declara as operações de travessia. No estilo GoF: temProximo(): boolean e proximo(): T. Na SPL do PHP: current(), next(), rewind(), valid(), key(). Em TypeScript: o método [Symbol.iterator]() retornando um objeto com next(): IteratorResult<T>.
  2. ConcreteIterator: mantém o cursor interno na coleção e implementa as operações de travessia. Cada chamada a criarIterador() retorna uma nova instância com seu próprio cursor — permitindo múltiplas travessias simultâneas independentes.
  3. Aggregate (interface): declara o método de criação do iterador. Ex.: criarIterador(): IteradorLista<T>.
  4. ConcreteAggregate: a coleção concreta que implementa o Aggregate e retorna o ConcreteIterator adequado. Pode fornecer múltiplos tipos de iterador (frente, reverso, filtrado) sem alterar a interface da coleção.

Iterator nativo vs GoF

A estrutura GoF (com temProximo()/proximo() em classes separadas) é útil para aprender o padrão e para linguagens sem protocolos nativos de iteração. Em TypeScript moderno, o protocolo Symbol.iterator com generators (function*) é mais idiomático — ele cria automaticamente os estados de cursor via yield, sem precisar de uma classe ConcreteIterator separada. Em PHP, IteratorAggregate + Generator segue o mesmo princípio.

Estrutura

       «interface»
      IteradorLista<T>
  ┌──────────────────────────────┐
  │ + temProximo(): boolean      │
  │ + proximo(): T               │
  └──────────────────────────────┘
              ▲
  IteradorListaEncadeada<T>
  ┌──────────────────────────────┐
  │ - atual: No<T> | null        │
  │ + temProximo(): boolean      │
  │ + proximo(): T               │
  └──────────────────────────────┘
              ▲ cria
              │
       «interface»
       Agregado<T>
  ┌──────────────────────────────────────┐
  │ + criarIterador(): IteradorLista<T>  │
  └──────────────────────────────────────┘
              ▲
  ListaEncadeada<T> (ConcreteAggregate)
  ┌──────────────────────────────────────┐
  │ - cabeca: No<T> | null               │
  │ + adicionar(valor: T): void          │
  │ + criarIterador(): IteradorLista<T>  │
  └──────────────────────────────────────┘


Fluxo de uso (estilo GoF):

  const it = lista.criarIterador();
  while (it.temProximo()) {
    console.log(it.proximo());   // sem acessar cabeca, No nem proximo
  }

Fluxo nativo (TypeScript):

  for (const item of lista) { ... }  // usa Symbol.iterator implicitamente

Exemplos de código

Exemplo 1 — Lista encadeada com Iterator manual (estilo GoF)

Implementação clássica do padrão: IteradorLista<T> como interface, IteradorListaEncadeada<T> como ConcreteIterator e ListaEncadeada<T> como Aggregate. O cliente percorre a coleção sem nenhum acesso à estrutura interna de nós.

// ── Iterator interface (estilo GoF) ──────────────────────────
interface IteradorLista<T> {
  temProximo(): boolean;
  proximo(): T;
}

// ── Nó interno da lista ───────────────────────────────────────
// Classe privada do pacote — o cliente nunca a vê.
class No<T> {
  proximo: No<T> | null = null;
  constructor(readonly valor: T) {}
}

// ── ConcreteIterator ──────────────────────────────────────────
class IteradorListaEncadeada<T> implements IteradorLista<T> {
  private atual: No<T> | null;

  constructor(inicio: No<T> | null) {
    this.atual = inicio;
  }

  temProximo(): boolean {
    return this.atual !== null;
  }

  proximo(): T {
    if (this.atual === null) {
      throw new Error('Iterator: sem mais elementos');
    }
    const valor = this.atual.valor;
    this.atual = this.atual.proximo;
    return valor;
  }
}

// ── ConcreteAggregate ─────────────────────────────────────────
class ListaEncadeada<T> {
  private cabeca: No<T> | null = null;
  private cauda: No<T> | null = null;

  adicionar(valor: T): void {
    const no = new No(valor);
    if (this.cauda === null) {
      this.cabeca = this.cauda = no;
    } else {
      this.cauda.proximo = no;
      this.cauda = no;
    }
  }

  // Cada chamada retorna um iterador com cursor próprio.
  criarIterador(): IteradorLista<T> {
    return new IteradorListaEncadeada(this.cabeca);
  }
}

// ── Uso ──────────────────────────────────────────────────────
const lista = new ListaEncadeada<string>();
lista.adicionar('Maçã');
lista.adicionar('Banana');
lista.adicionar('Cereja');

const it = lista.criarIterador();
while (it.temProximo()) {
  console.log(it.proximo());
}
// Maçã
// Banana
// Cereja

// Dois iteradores simultâneos na mesma lista — cursores independentes:
const it1 = lista.criarIterador();
const it2 = lista.criarIterador();
console.log(it1.proximo()); // Maçã
console.log(it2.proximo()); // Maçã  (cursor independente)
console.log(it1.proximo()); // Banana

Exemplo 2 — Protocolo nativo: Symbol.iterator / Generator (TS) e IteratorAggregate / Generator (PHP)

As linguagens modernas incorporam o Iterator como protocolo nativo. Em TypeScript, implementar [Symbol.iterator]() como um generator (function* / método com yield) torna a coleção compatível com for...of, spread e desestruturação — sem precisar de uma classe ConcreteIterator separada. Em PHP, IteratorAggregate + Generator alcança o mesmo resultado com muito menos código do que os 5 métodos da interface Iterator.

// Lista encadeada com protocolo nativo Symbol.iterator via generator.
// Sem classes ConcreteIterator — o yield gerencia o cursor automaticamente.

class No<T> {
  proximo: No<T> | null = null;
  constructor(readonly valor: T) {}
}

class ListaEncadeada<T> {
  private cabeca: No<T> | null = null;
  private cauda:  No<T> | null = null;

  adicionar(valor: T): void {
    const no = new No(valor);
    if (!this.cauda) {
      this.cabeca = this.cauda = no;
    } else {
      this.cauda.proximo = no;
      this.cauda = no;
    }
  }

  // Generator que implementa Symbol.iterator — torna a lista iterável nativamente.
  *[Symbol.iterator](): Generator<T> {
    let atual = this.cabeca;
    while (atual !== null) {
      yield atual.valor;   // suspende aqui e entrega o valor ao chamador
      atual = atual.proximo;
    }
  }

  // Segundo generator: iterador filtrado (múltiplas estratégias de travessia).
  *filtrar(predicado: (item: T) => boolean): Generator<T> {
    for (const item of this) {   // reutiliza Symbol.iterator
      if (predicado(item)) yield item;
    }
  }
}

// ── Uso ──────────────────────────────────────────────────────
const lista = new ListaEncadeada<number>();
[3, 1, 4, 1, 5, 9, 2, 6].forEach(n => lista.adicionar(n));

// for...of usa Symbol.iterator implicitamente.
for (const n of lista) {
  process.stdout.write(n + ' ');
}
// 3 1 4 1 5 9 2 6

console.log();

// Spread também usa Symbol.iterator.
const arr = [...lista];
console.log(arr);   // [3, 1, 4, 1, 5, 9, 2, 6]

// Iterador filtrado: só valores ímpares.
for (const n of lista.filtrar(n => n % 2 !== 0)) {
  process.stdout.write(n + ' ');
}
// 3 1 1 5 9

console.log();

// Múltiplas travessias independentes simultâneas são seguras:
// cada chamada ao generator cria um novo objeto com cursor próprio.
const itA = lista[Symbol.iterator]();
const itB = lista[Symbol.iterator]();
console.log(itA.next().value);   // 3
console.log(itB.next().value);   // 3  (cursor independente)
console.log(itA.next().value);   // 1

Quando usar

  • Quando você tem uma coleção customizada e não quer expor sua estrutura interna (nós, cursores, índices) para quem a percorre. O Iterator provê o acesso sequencial sem vazar detalhes de implementação.
  • Quando precisar de múltiplas formas de travessia da mesma coleção — em frente, em reverso, em largura, em profundidade, filtrado. Cada estratégia se torna um Iterator separado retornado pela coleção.
  • Para tornar coleções customizadas compatíveis com loops nativos: em TypeScript, Symbol.iterator habilita for...of, spread e desestruturação; em PHP, IteratorAggregate habilita foreach — sem nenhuma mudança na API pública da coleção.
  • Quando o código de percorrimento precisa ser reutilizado em múltiplos lugares sem duplicação — o Iterator encapsula o loop numa única definição.

Quando evitar

  • Para arrays e coleções simples já nativamente iteráveis: o padrão adiciona complexidade sem benefício quando a coleção já é um array ou um tipo que o loop da linguagem já suporta nativamente.
  • Quando a coleção sempre será percorrida de um único modo: se não há variação de estratégia de travessia e a estrutura é estável, um método paraArray() que retorna um array é mais simples e suficiente.

Prós e contras

Prós

  • Desacopla o algoritmo de travessia da estrutura de dados — trocar de lista encadeada para árvore não quebra o cliente.
  • Suporte a múltiplos iteradores simultâneos e independentes na mesma coleção, cada um com seu próprio cursor.
  • Múltiplas estratégias de travessia (frente, reverso, filtrado) sem modificar a interface da coleção.
  • Integração nativa com loops da linguagem via Symbol.iterator (TS) e Iterator/IteratorAggregate (PHP).
  • Generators eliminam a classe ConcreteIterator separada, reduzindo drasticamente o código necessário.

Contras

  • Overhead de criação de objeto iterador para coleções simples onde um índice seria suficiente.
  • Modificar a coleção durante a iteração pode invalidar o cursor do iterador — comportamento indefinido que exige cuidado.
  • Iteradores baseados em classes (estilo GoF) exigem mais código do que a abordagem funcional com generators.

Armadilhas comuns

1. Invalidar o iterador ao modificar a coleção durante a iteração

A armadilha mais perigosa: adicionar ou remover elementos da coleção enquanto um iterador a percorre pode pular elementos, visitar o mesmo elemento duas vezes ou causar exceções de acesso nulo. Em Java isso gera ConcurrentModificationException; em TypeScript e PHP o comportamento é indefinido e silencioso — o pior tipo de bug.

Regra de ouro: nunca modifique a coleção durante a iteração com o mesmo iterador. Se precisar filtrar e remover elementos, colete os índices ou itens a remover numa lista separada e modifique a coleção após o loop ter terminado.

2. Vazar a estrutura interna pelo Iterator

Um Iterator que expõe os nós internos (No<T>) em vez dos valores (T) derrota o propósito do padrão. O cliente acaba acessando it.proximo().valor e depois .proximo diretamente — o encapsulamento foi quebrado. O Iterator deve expor somente o tipo de dado que o cliente precisa, nunca os containers internos.

3. Estado compartilhado entre iteradores

Se uma coleção armazena o cursor interno como campo de instância (em vez de retornar um objeto iterador separado com cursor próprio), dois loops concorrentes "roubam" posições um do outro. A solução: cada chamada a criarIterador() ou getIterator() deve retornar um novo objeto com seu próprio cursor. Generators em TypeScript e PHP resolvem isso automaticamente — cada invocação do generator cria uma instância independente.

4. Esquecer o rewind em PHP

Ao usar a interface Iterator do PHP (não IteratorAggregate), o foreach chama rewind() antes de iniciar o loop. Se rewind() não reinicializa corretamente o cursor, o segundo foreach na mesma instância começa do ponto onde o primeiro parou — comportamento surpreendente. Com IteratorAggregate + Generator isso não ocorre: cada foreach chama getIterator() e obtém um Generator novo com cursor zerado.

Padrões relacionados

O Iterator se integra com padrões estruturais que organizam coleções hierárquicas:

O Composite organiza objetos em estruturas de árvore (parte-todo); o Iterator é o complemento natural para percorrê-la. Um Iterator pode implementar travessia em largura (BFS) ou em profundidade (DFS) sobre a árvore de Composites — o cliente usa a mesma interface de iteração independentemente da estratégia de percorrimento escolhida. O Visitor é o par do Iterator em operações sobre estruturas: enquanto o Iterator percorre sequencialmente e entrega cada elemento ao chamador, o Visitor percorre a estrutura e aplica uma operação específica em cada tipo de nó — separando o algoritmo de processamento da estrutura do Composite.