Il problema: nuove operazioni su classi chiuse
Hai una gerarchia di nodi AST (Abstract Syntax Tree) per un linguaggio di template: TextNode, VariableNode, IfNode, ForNode, IncludeNode. Devi aggiungere operazioni diverse su questi nodi: rendering HTML, compilazione in PHP, conteggio delle variabili usate, validazione della sintassi, pretty-printing. Se aggiungi un metodo per ogni operazione a ogni classe nodo, le classi esplodono di complessità. E peggio: ogni nuova operazione richiede di modificare tutte le classi nodo.
Il Visitor Pattern inverte la responsabilità: invece di aggiungere metodi ai nodi, crei un nuovo oggetto Visitor per ogni operazione. Il visitor "visita" ogni nodo e il nodo gli dice "sono un IfNode, trattami come tale". Questa tecnica si chiama double dispatch: il comportamento dipende sia dal tipo del visitor che dal tipo del nodo.
Cos'e il Visitor Pattern: definizione formale
Il Gang of Four definisce il Visitor come un pattern comportamentale che "rappresenta un'operazione da eseguire sugli elementi di una struttura di oggetti. Il Visitor permette di definire nuove operazioni senza cambiare le classi degli elementi su cui opera". La struttura:
-
Visitor: interfaccia con un metodo
visitper ogni tipo di elemento (visitTextNode(TextNode $n),visitIfNode(IfNode $n)) -
ConcreteVisitor: implementa l'operazione specifica per ogni tipo di nodo (es.
HtmlRendererVisitor,VariableCounterVisitor) -
Element: interfaccia con un metodo
accept(Visitor $v) -
ConcreteElement: ogni nodo implementa
accept()chiamando$v->visitXxx($this)
Il double dispatch spiegato
PHP (come Java e C#) supporta solo il single dispatch: il metodo chiamato dipende dal tipo dell'oggetto su cui viene chiamato, non dal tipo dei parametri. Il Visitor simula il double dispatch con due chiamate polimorfiche:
- Il client chiama
$node->accept($visitor)— primo dispatch basato sul tipo del nodo - Dentro
accept(), il nodo chiama$visitor->visitIfNode($this)— secondo dispatch basato sul tipo del visitor
Il risultato: il comportamento dipende da entrambi i tipi. Un HtmlRendererVisitor che visita un IfNode produce HTML diverso da un VariableCounterVisitor che visita lo stesso IfNode.
Esempio teorico: analisi di un AST
Con il Visitor puoi aggiungere operazioni illimitate senza toccare i nodi:
-
HtmlRendererVisitor: trasforma l'AST in HTML.visitTextNode()restituisce il testo,visitIfNode()valuta la condizione e renderizza il branch corretto. -
PhpCompilerVisitor: compila l'AST in codice PHP.visitIfNode()generaif ($condition) { ... }. -
VariableCollectorVisitor: raccoglie tutte le variabili usate.visitVariableNode()aggiunge il nome alla lista. -
SecurityAuditorVisitor: cerca pattern pericolosi.visitIncludeNode()verifica che il path non contenga input dell'utente non sanitizzato.
Ogni nuova operazione e una nuova classe Visitor. I nodi non cambiano mai. Questo e il potere del pattern: l'asse delle operazioni e aperto all'estensione.
Visitor nel mondo reale: Rector e PHPStan
Rector (il tool di refactoring automatico per PHP) e costruito interamente sul Visitor Pattern. Ogni regola di refactoring e un Visitor che attraversa l'AST del codice PHP. RenameClassRector visita i nodi di tipo Name e li rinomina. AddReturnTypeDeclarationRector visita i nodi ClassMethod e aggiunge i return type. Aggiungere una nuova regola significa creare un nuovo Visitor, senza toccare il parser o i nodi AST.
PHPStan usa lo stesso principio: ogni regola di analisi statica e un visitor che ispeziona nodi specifici dell'AST cercando pattern problematici.
Quando usare il Visitor Pattern
- Usa il Visitor quando la struttura degli oggetti e stabile ma le operazioni cambiano spesso
- Usa il Visitor quando vuoi aggiungere operazioni a classi che non puoi o non vuoi modificare
- Usa il Visitor per separare algoritmi complessi dalle strutture dati su cui operano
- Non usare il Visitor se la gerarchia di classi cambia spesso: ogni nuovo tipo richiede un nuovo metodo in tutti i visitor
- Non usare il Visitor se hai 2-3 operazioni semplici: un metodo nella classe e più diretto
Il Visitor Pattern e il pattern della separazione operazione-struttura. Quando la struttura e stabile e le operazioni crescono, il Visitor evita di riaprire classi chiuse e mantiene ogni operazione in un'unita coesa e testabile.
Top comments (0)