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.
Applicabilità
[modifica | modifica wikitesto]Visitor è utile quando:
- una struttura di oggetti è costituita da molte classi con interfacce diverse ed è necessario che l'algoritmo esegua su ogni oggetto un'operazione differente a seconda della classe concreta dell'oggetto stesso,
- è necessario eseguire svariate operazioni indipendenti e non relazionate tra loro sugli oggetti di una struttura composta, ma non si vuole sovraccaricare le interfacce delle loro classi. Riunendo le operazioni correlate in ogni Visitor è possibile inserirle nei programmi solo dove necessario,
- le classi che costituiscono la struttura composta sono raramente suscettibili di modifica, ma è necessario aggiungere spesso operazioni sui rispettivi oggetti. Ogni intervento sulle operazioni richiede la modifica o l'estensione di un Visitor, mentre ogni modifica alle classi della struttura vincola alla ridefinizione delle interfacce di tutti i Visitor, compito che può risultare estremamente complesso nei progetti di una certa dimensione.
Struttura
[modifica | modifica wikitesto]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
[modifica | modifica wikitesto]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
[modifica | modifica wikitesto]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
[modifica | modifica wikitesto]Element
definisce un'operazione accept
utilizzata per "accettare" un Visitor
passato come parametro.
ConcreteElement
[modifica | modifica wikitesto]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
[modifica | modifica wikitesto]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.
Funzionamento
[modifica | modifica wikitesto]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.
Conseguenze
[modifica | modifica wikitesto]Flessibilità delle operazioni
[modifica | modifica wikitesto]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.
Organizzazione delle operazioni
[modifica | modifica wikitesto]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.
Rigidità della gerarchia di classi
[modifica | modifica wikitesto]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.
Visita di vari tipi di classe
[modifica | modifica wikitesto]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à.
Mantenimento di uno stato
[modifica | modifica wikitesto]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.
Violazione dell'incapsulamento
[modifica | modifica wikitesto]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.
Implementazione
[modifica | modifica wikitesto]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.
Inoltro doppio
[modifica | modifica wikitesto]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.
Attraversamento della struttura
[modifica | modifica wikitesto]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:
- un
Visitor
, - un
ObjectStructure
, - un Iterator esterno.
Bibliografia
[modifica | modifica wikitesto]- Design Patterns: Elements of Reusable Object-Oriented Software di Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides, 2002, ISBN 88-7192-150-X.
Voci correlate
[modifica | modifica wikitesto]Altri progetti
[modifica | modifica wikitesto]- Wikimedia Commons contiene immagini o altri file sul visitor
Collegamenti esterni
[modifica | modifica wikitesto]- (EN) Implementazione in Java, su codeguru.com.
- (EN) Implementazione in Java con reflection (XML), su surguy.net.
- (EN) Implementazione in C++, su swe.uni-linz.ac.at. URL consultato il 20 maggio 2006 (archiviato dall'url originale il 7 luglio 2006).
- (EN) Altra implementazione in C++, su cs.huji.ac.il.