I visitor
Il visitor permette di definire una nuova operazione su una struttura gerarchica ad oggetti senza modificare le classi della struttura. Permette inoltre di raggruppare il codice relativo ad una operazione senza la necessità di distribuirlo all'interno degli oggetti della gerarchia. Utilizzando i visitor si ottengono i vantaggi e gli svantaggi della metodologia di programmazione funzionale, pur continuando a programmare ad oggetti.
Fintantoché la gerarchia non cambia, possiamo
aggiungere funzionalità semplicemente definendo un nuovo visitor.
Questo costrutto è molto utile nella costruzione degli interpreti
e dei compilatori quando il linguaggio è già ben definito
e non modificabile, infatti partendo da una gerarchia ben definita è
possibile costruire tutte le funzionalità (analisi lessicale, parsing,
stampa, valutazione, compilazione, traduzione...) semplicemente aggiungendo
nuovi visitor. La metodologia di programmazione ad oggetti richiederebbe
di modificare ogni classe della gerarchia.
Tramite il double dispatching è il visitor
che si incarica di eseguire le operazioni al posto delle classi.
Una prima osservazione: anziché visit_IdentSexp(IdentSexp e) è possibile utilizzare visit(IdentSexp e) poiché la prima notazione è ridondante. I metodi vengono differenziati grazie al parametro di ingresso. Si ottengono dei vantaggi che vedremo in seguito.
Il problema fondamentale del visitor è che
se modifichiamo la gerarchia siamo costretti a modificare tutti i visitor
esistenti.
Ulteriori problemi, conseguenze di quello precedente, si hanno se a
partire da una gerarchia già esistente e non modificabile
vogliamo creare una gerarchia più ampia.
Per fare un discorso più chiaro supponiamo di avere già una gerarchia costituita da Sexp (astratta), ConsSexp ed IdentSexp (concrete), più un visitor astratto SexpVisitor. Vogliamo aggiungere alla gerarchia le nuove classi InfixSexp (derivata da ConsSexp), AndSexp e PlusSexp (derivate da InfixSexp). Per queste ho bisogno dei visitor concreti RenameVisitor, EvalVisitor ed EngineVisitor che derivo tutti da NewVisitor astratto. RenameVisitor necessita delle visit per IdentSexp ed InfixSexp. EvalVisitor necessita delle visit per IdentSexp, PlusSexp ed InfixSexp. EngineVisitor necessita delle visit per IdentSexp, AndSexp ed InfixSexp. Negli ultimi due visitor la InfixSexp serve per segnalare condizioni di errore. Il tutto è riassunto graficamente dalle seguenti figure:
figura 4.
figura 5.
Problema 1.
NewVisitor deve derivare da SexpVisitor, altrimenti non sarebbe possibile utilizzare la classe IdentSexp poiché il suo metodo accept prende solo SexpVisitor o una sua sottoclasse. Nelle nuove classi il parametro di ingresso della accept deve essere un SexpVisitor altrimenti definiremmo un metodo che non sovrascrive quello della superclasse Sexp e dunque non verrebbe richiamato se l'oggetto su cui facciamo una accept è referenziato da una variabile Sexp (si ha cast statico). Nel metodo accept delle nuove classi della gerarchia siamo costretti a fare un cast del visitor. Infatti il metodo accept di una AndSexp contiene visitor.visit(this). Quando viene eseguita questa istruzione, qualunque sia il visitor referenziato da visitor, Java fa un cast statico ad un SexpVisitor. Questo contiene la visit(ConsSexp) e dunque viene eseguita la visit(ConsSexp) del visitor anziché la visit(AndSexp).
Se lasciassimo i nomi completi delle visit, l'errore
avverrebbe ancora prima: non è possibile definire i metodi accept
delle nuove classi in modo che sovrascrivano la accept di SexpVisitor.
Infatti in SexpVisitor non esiste la visitAnd()... Dunque il parametro
di ingresso della visitAnd deve essere un NewVisitor. Anche con dei cast
all'invocazione della accept sulla Sexp (descritto di seguito) non si risolverebbe
il problema perché un metodo accept di un PlusSexp non potrà
mai chiamare una visitInfix() a causa della differenza dei nomi. Al limite
bisognerebbe chiamare esplicitamente il metodo visitInfix() dopo aver fatto
i controlli ed i cast.
Se il riferimento è un Sexp, anche se referenzia un AndSexp viene comunque chiamata la accept dell'SexpVisitor, cioè quella della ConsSexp.
Tutti questi problemi sono dovuti a cast statici dell'interprete. Infatti i metodi che in c++ vengono chiamati virtuali sono quelli che una classe figlia sovrascrive, ovvero che hanno una stessa signature: stesso numero di argomenti e valore di ritorno e stesso tipo di argomenti. Nel nostro caso non sovrascriviamo nessun metodo, ma ne definiamo di nuovi poiché il tipo degli argomenti della visit cambia.
Per interpretare correttamente questi casi servirebbe una concezione più ampia di metodi virtuali: la tabella dei metodi virtuali dovrebbe contenere, oltre ai metodi delle superclassi sovrascritti, anche quelli delle superclassi o della stessa classe che si differenziano per il parametro di chiamata se questo parametro fa parte della stessa gerarchia.
Per farlo funzionare correttamente bisognerebbe fare,
oltre al cast sul visitor all'interno del metodo accept, un cast alla invocazione
della accept:
((AndSexp)and).accept(eval);
La cosa è alquanto scomoda e svilisce notevolmente i vantaggi
di una programmazione ad oggetti: ogni volta che si fa una accept bisogna
controllare quale è la classe dell'oggetto e fare un cast. Sarebbe
dunque inutile poter referenziare le classi tramite un riferimento comune
astratto Sexp.
Problema 2.
A questo punto sorge un ulteriore problema. Abbiamo
una molteplicità di visitor ed alcuni visitor sono interessati solo
ad una parte della gerarchia. Ad esempio vogliamo che la visit(InfixSexp)
del RenameVisitor raccolga anche le chiamate fatte da una PlusSexp e da
una AndSexp per non duplicare inutilmente il codice (si pensi a casi con
50 o più classi). Se all'interno del metodo accept della PlusSexp
facciamo un cast ad un NewVisitor, verrà chiamata la visit(ConsSexp)
del NewVisitor. Aggiungendo il cast sull'oggetto PlusSexp alla chiamata
della accept viene eseguita la visit(PlusSexp) del NewVisitor. In entrambi
i casi non è ciò che vogliamo. Si ripone il problema del
cast all'interno della accept: prima di fare il cast del visitor dobbiamo
controllare che visitor è. Così facendo addirittura si ha
errore in compilazione perché Java vede una ambiguità tra
la visit(InfixSexp) del RenameVisitor e la visit(PlusSexp) del NewVisitor.
Ambiguità che si risolve eliminando la visit(PlusSexp) dal NewVisitor.
Così non serve più il controllo sul visitor, né il
cast sull'oggetto prima della accept. Però nell'Engine viene sempre
chiamato visit(Infix) anche se visitiamo una AndSexp.
L'unica soluzione è fare più di un visitor astratto e comunque è necessario il cast sul visitor, oltre al controllo su quale cast fare. Praticamente all'interno dello stesso visitor astratto non ci va bene avere contemporaneamente la visit(PlusSexp) e la visit(InfixSexp) (questo per il RenameVisitor e per l'EngineVisitor) oppure la visit(AndSexp) e la visit(InfixSexp) (questo per l'EvalVisitor e sempre per il RenameVisitor).
Per questi motivi abbiamo deciso di fare un visitor astratto StructureSexpVisitor che comprende solo gli elementi tipo visit(InfixSexp). Da questo abbiamo derivato altri due visitor astratti: EvalSexpVisitor e PrologSexpVisitor. Da questi abbiamo derivato i visitor concreti. Si sarebbero potuti evitare i visitor astratti, ma la gerarchia avrebbe dovuto conoscere i visitor concreti e fare un cast a quelli. Si perderebbe l'information hiding, ma basterebbe modificare un solo visitor se aggiungiamo un oggetto alla gerarchia.
Naturalmente la soluzione più veloce per il programmatore sarebbe modificare le classi della gerarchia esistente (la ConsSexp e la IdentSexp nel nostro caso). Eventualmente si potrebbero realizzare dei metodi accept differenziati: acceptEngine(), acceptEval(), acceptRename(). Il codice sarebbe ancora racchiuso tutto all'interno di un visitor, però la filosofia di chiamata sarebbe quella ad oggetti: ogni oggetto contiene il metodo specializzato. Sparirebbe la necessità dei visitor astratti.
Conclusioni.
I visitor sono un utile strumento di sviluppo, ma
i linguaggi attuali non li supportano pienamente. Per questo durante la
programmazione bisogna arrivare a dei compromessi tra information hiding
e modularità (semplicità nel modificare la gerarchia). Se
i linguaggi controllassero dinamicamente l'oggetto passato come parametro
non sarebbe più nemmeno necessario il double dispatching ed i metodi
accept. I visitor si utilizzerebbero semplicemente con
nomeVisitor.visit(oggetto);
Nelle condizioni attuali bisognerebbe fare un cast esplicito:
engine.visit((AndSexp)oggetto);
ma per poter fare tale cast è necessario fare un controllo (tanti
if quante sono le possibili classi utilizzate da un visitor).