DEV Community

Cover image for Iterator Pattern: attraversare collezioni senza esporne la struttura
Dev-Iadicola
Dev-Iadicola

Posted on • Originally published at iadicola.it

Iterator Pattern: attraversare collezioni senza esporne la struttura

Il problema: attraversamento accoppiato alla struttura

Hai una classe UserCollection che internamente usa un array PHP. Il codice client fa for ($i = 0; $i < $collection->count(); $i++) { $user = $collection->getAt($i); ... }. Funziona, ma il client sa che la collezione e indicizzata per posizione. Se domani la struttura interna cambia — un albero, una linked list, un result set paginato dal database — il codice client si rompe.

Il problema e l'accoppiamento tra attraversamento e struttura. Chi usa la collezione non dovrebbe sapere come e organizzata internamente. Dovrebbe solo poter dire "dammi il prossimo elemento" fino a che non ce ne sono più. L'Iterator Pattern formalizza questa separazione.

Cos'e l'Iterator Pattern: definizione formale

Il Gang of Four definisce l'Iterator come un pattern comportamentale che "fornisce un modo per accedere sequenzialmente agli elementi di un oggetto aggregato senza esporne la rappresentazione sottostante". La struttura prevede:

  • Iterator: l'interfaccia con metodi come current(), next(), hasNext(), rewind()
  • ConcreteIterator: implementa l'attraversamento per una struttura dati specifica
  • Aggregate: l'interfaccia della collezione con un metodo createIterator()
  • ConcreteAggregate: la collezione che crea il proprio iteratore

In PHP, il pattern e talmente fondamentale da essere integrato nel linguaggio: le interfacce Iterator e IteratorAggregate di SPL permettono a qualsiasi oggetto di essere usato in un foreach. Se la tua classe implementa Iterator, il foreach funziona automaticamente — il linguaggio stesso usa l'Iterator Pattern.

Esempio teorico: paginazione trasparente dal database

Immagina di dover processare 100.000 utenti dal database. Caricarli tutti in memoria e impraticabile: servirebbero gigabyte di RAM. La soluzione e un PaginatedUserIterator che carica gli utenti a blocchi di 1.000:

  • Il costruttore riceve la query base e la dimensione del blocco
  • current() restituisce l'utente corrente dal blocco caricato
  • next() avanza al prossimo utente; quando il blocco finisce, carica il blocco successivo dal database
  • valid() restituisce false quando non ci sono più blocchi

Il codice client e un semplice foreach ($users as $user) — non sa e non deve sapere che sotto il cofano stanno avvenendo 100 query da 1.000 record ciascuna. La memoria usata e costante (1.000 oggetti alla volta), indipendentemente dal numero totale di utenti. Questo e il potere dell'Iterator: disaccoppia il "cosa fare con ogni elemento" dal "come ottenere il prossimo elemento".

Generator PHP: Iterator senza boilerplate

PHP offre i generator come zucchero sintattico per creare Iterator senza implementare l'intera interfaccia. Un generator e una funzione che usa yield invece di return:

function paginatedUsers(int $chunkSize): Generator { $offset = 0; while ($chunk = fetchUsersChunk($offset, $chunkSize)) { foreach ($chunk as $user) { yield $user; } $offset += $chunkSize; } }

Il generator produce un oggetto Generator che implementa Iterator. Il foreach (paginatedUsers(1000) as $user) funziona identicamente all'esempio precedente, ma con 6 righe invece di una classe intera. I generator sono la forma moderna dell'Iterator Pattern in PHP.

Lazy evaluation: calcolare solo cio che serve

I generator sono lazy per natura: producono il valore successivo solo quando richiesto. Se il foreach fa break dopo 10 elementi, il generator non carichera mai gli altri 99.990. Questa lazy evaluation e impossibile con un array: array_map e array_filter processano sempre l'intera collezione, anche se serve solo il primo risultato.

La combinazione di generator e yield from permette di comporre iteratori: un generator che filtra, uno che trasforma, uno che limita — ogni step e lazy e la pipeline processa un elemento alla volta, senza mai tenere l'intera collezione in memoria.

Esempio teorico: file processing riga per riga

Un file CSV di 2 GB non può essere caricato in memoria con file(). Un CsvIterator apre il file, legge una riga alla volta con fgets(), la parsa e la yielda. Il codice client fa foreach (new CsvIterator('data.csv') as $row) e processa 2 GB di dati con 10 KB di memoria.

Questo approccio si combina con i generator per creare pipeline di trasformazione:

  • readCsv('data.csv') — generator che produce righe grezze
  • filterActive($rows) — generator che filtra solo le righe con status attivo
  • mapToEntity($rows) — generator che trasforma array in oggetti
  • batchInsert($entities, 500) — generator che raggruppa in batch e inserisce nel database

Ogni step processa un elemento alla volta. L'intero file da 2 GB viene processato con un consumo di memoria costante. Senza l'Iterator Pattern (e i generator come sua implementazione moderna), questo sarebbe impossibile o richiederebbe codice molto più complesso.

Iterator e interfacce SPL in PHP

PHP offre diverse interfacce e classi nell'estensione SPL per casi d'uso specifici:

  • FilterIterator: wrappa un iteratore e restituisce solo gli elementi che soddisfano una condizione
  • LimitIterator: wrappa un iteratore e restituisce solo i primi N elementi
  • AppendIterator: concatena più iteratori in una sequenza unica
  • RecursiveIteratorIterator: attraversa strutture ricorsive (alberi di directory, menu annidati)
  • CachingIterator: aggiunge lookahead — sai se c'e un elemento successivo prima di avanzare

Queste classi si compongono come Decorator: new LimitIterator(new FilterIterator(new ArrayIterator($data)), 0, 10) crea un iteratore che filtra e limita a 10 risultati. E OOP puro, senza un singolo for nel codice client.

Quando usare l'Iterator Pattern

  • Usa l'Iterator quando la struttura dati e complessa (alberi, grafi, risultati paginati) e vuoi nasconderla al client
  • Usa l'Iterator quando i dati sono troppi per stare in memoria e servono lazy loading o streaming
  • Usa l'Iterator quando vuoi comporre pipeline di trasformazione senza allocare array intermedi
  • Usa i Generator come implementazione moderna quando non serve un'intera classe Iterator
  • Non usare l'Iterator se hai un semplice array PHP: il foreach su array e già ottimale
  • Non usare l'Iterator se hai bisogno di accesso random (per indice): l'Iterator e sequenziale per definizione

L'Iterator Pattern e così fondamentale che ogni linguaggio moderno lo integra nella propria sintassi. In PHP, il foreach e l'Iterator Pattern reso trasparente. Ma capire il pattern dietro il costrutto sintattico permette di crearne di nuovi — iteratori lazy, paginati, filtrati — che risolvono problemi reali con eleganza e efficienza.


👉 Leggi l'articolo completo su iadicola.it

Top comments (0)