Il Visitor è un design pattern comportamentale utilizzato in informatica nella programmazione orientata agli oggetti. Permette di separare un algoritmo dalla struttura di oggetti composti a cui è applicato, in modo da poter aggiungere nuove operazioni e comportamenti senza dover modificare la struttura stessa.
Visitor è utile quando:
Il diagramma delle classi in UML rappresenta una struttura esemplificativa in Java.
La classe Client
, pur non essendo parte integrante del pattern, è comunque illustrata per mostrare come possa interagire con l'interfaccia Visitor
e la struttura di oggetti ObjectStructure
. In questo caso definisce due metodi walk
che si occupano di iterare su ogni oggetto della struttura, visitandolo rispettivamente con un ConcreteVisitor1
e un ConcreteVisitor2
utilizzati attraverso l'interfaccia comune Visitor
.
Visitor
Visitor
dichiara un metodo visit
per ogni ConcreteElement
appartenente alla struttura di oggetti, in modo che ogni oggetto della struttura possa invocare il metodo visit
appropriato passando un riferimento a sé (this
) come parametro.
Questo permette al Visitor
di identificare la classe che ha chiamato il metodo visit
, eseguire il comportamento corrispondente e accedere all'oggetto attraverso la sua specifica interfaccia.
ConcreteVisitor
ConcreteVisitor
implementa le operazioni visit
dichiarate da Visitor
perché agiscano come desiderato sulle rispettive classi. Inoltre fornisce il contesto dell'algoritmo e ne mantiene in memoria lo stato, che spesso accumula i risultati parziali ottenuti durante l'attraversamento della struttura.
Element
Element
definisce un'operazione accept
utilizzata per "accettare" un Visitor
passato come parametro.
ConcreteElement
ConcreteElement
implementa la accept
definita da Element
. In generale accept
chiama il metodo visit
del Visitor
ricevuto, passando come parametro un riferimento a sé.
ObjectStructure
ObjectStructure
contiene ed elenca gli elementi. Quando necessario può fornire un'interfaccia d'alto livello che permetta al Visitor
di visitare i singoli Element
. Può essere implementata applicando il pattern Composite, oppure utilizzando una collezione come ad esempio un array o qualsiasi altra struttura dati.
Un client che voglia utilizzare un Visitor
deve creare un oggetto ConcreteVisitor
e utilizzarlo per attraversare la struttura, chiamando il metodo accept
di ogni oggetto. Ogni chiamata invoca nel ConcreteVisitor
il metodo corrispondente alla classe dell'oggetto chiamante, che passa sé stesso come parametro per fornire al Visitor
un punto d'accesso al proprio stato interno.
L'applicazione di questo design pattern permette di avere un'ampia flessibilità nell'aggiunta di nuove operazioni relative agli oggetti contenuti nella struttura. Per aggiungere operazioni è sufficiente creare un nuovo Visitor
che le definisca e i ConcreteVisitor
che le implementino, oppure creare direttamente un nuovo ConcreteVisitor
che implementi un'interfaccia Visitor
già esistente. Senza applicare il pattern si avrebbero una serie di funzionalità sparse in svariate classi, situazione che obbligherebbe a modificarle tutte ogni qualvolta si debba aggiungere un'operazione.
Un Visitor
raggruppa logicamente le operazioni correlate che possono essere eseguite su un gruppo di oggetti. Altri Visitor
possono raggruppare ulteriori operazioni, creando in modo semplice una divisione logica delle funzionalità, organizzata gerarchicamente nelle sottoclassi dei vari Visitor
. Di conseguenza le classi degli elementi possono essere semplificate e le strutture dati di un algoritmo possono essere nascoste all'interno del Visitor
corrispondente.
Se l'aggiunta di operazioni è semplificata, così non è per l'aggiunta di sottoclassi di Element
: ogni nuovo ConcreteElement
obbliga ogni interfaccia Visitor
a definire un nuovo metodo visit
relativo al nuovo tipo concreto e ogni ConcreteVisitor
a implementarlo.
Per questo motivo il pattern Visitor è meglio utilizzabile quando la gerarchia di elementi non è suscettibile di numerose modifiche. In caso contrario, la difficoltà nella manutenzione delle classi Visitor
rende probabilmente più conveniente gestire le funzionalità sugli oggetti in modo tradizionale, ovvero incorporandole negli oggetti stessi.
Al contrario del pattern Iterator, un Visitor non è vincolato al tipo degli oggetti presenti nella struttura che deve attraversare. Infatti un Iterator può accedere solo a oggetti di un certo tipo e relative sottoclassi, mentre Visitor può agire anche su oggetti le cui classi non abbiano tra loro alcuna relazione di ereditarietà.
Invece di mantenere uno stato come variabile globale o come parametro aggiuntivo passato ai vari metodi di attraversamento, è possibile incapsularlo all'interno di un Visitor
e aggiornarlo ad ogni visita.
Poiché su ogni ConcreteElement
deve essere possibile l'azione di un ConcreteVisitor
, è necessario che l'interfaccia degli elementi permetta l'accesso e la modifica dello stato interno, implementando metodi pubblici. In questo modo diventa responsabilità dei programmatori non modificare un oggetto Element
in un punto del programma in cui non dovrebbe essere visibile. In Java è possibile ovviare parzialmente al problema utilizzando la visibilità package, raggruppando sia i visitor che gli elementi nello stesso package.
Ogni struttura ad oggetti deve avere una classe Visitor
corrispondente, che definirà un metodo visit
per ogni classe ConcreteElement
facente parte della struttura. I metodi visit
possono utilizzare la tecnica dell'overloading e quindi avere lo stesso nome e differire solo per il tipo dell'argomento,
visit (ConcreteElementA a){ } visit (ConcreteElementB a){ }
oppure specificare nel nome stesso il tipo su cui agiscono
visitConcreteElementA (ConcreteElementA a){ } visitConcreteElementB (ConcreteElementB a){ }
L'overloading può risultare più comodo ed elegante, anche se meno chiaro dal punto di vista della leggibilità del codice.
Il pattern Visitor è utile per ottenere un comportamento di inoltro doppio (double dispatch) nei linguaggi di programmazione che non lo supportano nativamente, ovvero i linguaggi a inoltro singolo.
L'inoltro singolo prevede che il risultato di un'operazione dipenda da due criteri: il nome dell'operazione e il tipo del destinatario. Nell'inoltro doppio l'operazione viene determinata dal suo nome e dai tipi di due destinatari (da qui il nome).
Nell'esecuzione di un Visitor, l'operazione chiamata dipende dal proprio nome e da due destinatari: il tipo di ConcreteVisitor
utilizzato e il tipo ConcreteElement
che viene visitato.
I linguaggi che implementano direttamente il double dispatch non hanno necessità di un pattern Visitor.
L'attraversamento della struttura può essere effettuato da varie componenti del pattern. Nell'esempio rappresentato dall'immagine è il Client
che si occupa di iterare con un ciclo for
su tutti gli elementi di ObjectStructure
, comodo quando la struttura è semplice. Più in generale l'iterazione può essere responsabilità di:
Visitor
,ObjectStructure
,