Home > GOF Pattern, java > GoF Patterns: Visitor

GoF Patterns: Visitor

2 aprile 2013

Translate in English with Google Translate
In questo articolo tratterò il pattern Visitor

Motivazione
Si tratta di un pattern comportamentale basato su oggetti e viene utilizzato per eseguire delle operazioni sugli elementi di una struttura. L’utilizzo di questo pattern consente di definire le operazioni di un elemento senza doverlo modificare.
Ma com’è possibile?
Non mi riferisco alla tecnica di decorazione, trattata in un articolo precedente, ma mi riferisco ad un’altra tecnica di “arricchimento” del codice.
Solitamente ogni classe definisce le proprie proprietà e le proprie operazioni nel rispetto del principio della singola responsabilità (SRP) ed usando il concetto di ereditarietà può condividere le operazioni alle classi figlie.
Ma cosa succede se ci accorgiamo a posteriori che dobbiamo introdurre una nuova operazione?

  • Se le operazioni sono state definite a livello di classe, l’introduzione di un nuovo metodo comporterà la modifica della classe interessata, violando il principio open-closed (OCP).
  • Se le operazioni sono state definite a livello di interfaccia, l’introduzione di un nuovo metodo comporterà la modifica di tutte le classi figlie.

Ovviamente se questa situazione si presenta frequentemente, la manutenzione del codice non sarà agevole.

Per evitare questo problema sarà possibile seguire un’altra strada, ossia disaccoppiare gli oggetti che definiscono lo stato dagli oggetti che definiscono il comportamento ed in questo modo sarà più semplice inserire nuovi metodi.
Il pattern Visitor ci consente di implementare questa separazione tra stato e comportamento e realizzare il legame tra questi oggetti tramite la definizione di 2 metodi presenti nelle due strutture.

  1. Nella prima struttura, che definisce lo stato, è presente il metodo accept() che invoca il metodo visit()
  2. Nella seconda struttura, che definisce il comportamento, è presente il metodo visit()

In questo modo sarà possibile aggiungere nuove operazioni semplicemente definendo nuove classi nella seconda struttura che si occuperà poi di elaborare lo stato della prima.

Vediamo la rappresentanzione UML usando il Class Diagram:

Visitor Base

Visitor Base

Pertanto, parlando in termini del pattern Visitor:
In base alla competenza:

  1. la prima struttura definisce gli Element che detengono lo stato
  2. la seconda struttura definisce i Visitor che detengono i comportamenti

In base all’ordine di invocazione:

  1. Il Client invoca il metodo accept() presente nell’Element passandogli in ingresso un oggetto Visitor
  2. L’Element invoca il metodo visit() del Visitor passandogli se stesso (oggetto this) come parametro.
  3. Il Visitor, disponendo della referenza all’Element (tramite l’oggetto this) accede alle proprietà dell’Element ed eseguire le operazioni.

Vediamo la rappresentanzione UML usando il Sequence Diagram:

Simple Visitor Interaction

Simple Visitor Interaction

Questo pattern utilizza la tecnica del Double Dispatch, che ho trattato in un articolo precedente, al fine di consentire questo scambio di messaggi tra l’Element ed il Visitor, pertanto risulta un po’ complesso considerando che utilizza polimorfismo, overriding ed overloading.

Partecipanti e Struttura
Questo pattern è composto dai seguenti partecipanti:

  • Element: definisce il metodo accept() che prende un Visitor come argomento
  • ConcreteElement: implementa un oggetto Element che prende un Visitor come argomento
  • ObjectStructure: contiene una collezione di Element che può essere visitata dagli oggetti Visitor
  • Visitor: dichiara un metodo visit() per ogni Element; il nome del metodo ed il parametro identificano la classe Element che ha effettuato l’invocazione.
  • ConcreteVisitor: implementa il metodo visit() e definisce l’algoritmo da applicare per l’Element passato come parametro

Vediamo come si presenta il Pattern Visitor utilizzando il Class Diagram in UML:

Visitor Pattern Structure

Visitor Pattern Structure

Conseguenze
Tale pattern presenta i seguenti vantaggi/svantaggi:

  • Facilità nell’aggiungere nuovi Visitor: definendo un nuovo Visitor sarà possibile aggiungere una nuova operazione ad un Element
  • Difficoltà nell’aggiungere nuovi Element: definire un nuovo Element comporterà la modifica dell’interfaccia Visitor e di tutte le implementazioni
  • Separazione tra stato ed algoritmi: gli algoritmi di elaborazioni sono nascosti nelle classi Visitor e non vengono esposti nelle classi Element.
  • Iterazione su struttura eterogenea: la classe Visitor è in grado di accedere a tipi diversi, senza la necessità che tra di essi ci sia un vincolo di parentela. In poche parole, il metodo visit() può definire come parametro un tipo X oppure un tipo Y senza che tra di essi ci sia alcuna relazione di parentela, diretta o indiretta.
  • Accumulazione dello stato: un Visitor può accumulare delle informazioni di stato a seguito dell’attraversamento degli Element.
  • Violazione dell’incapsulamento: i Visitor devono poter accedere allo stato degli Element e questo può comportare la violazione dell’incapsulamento.

Implementazione
Il pattern Visitor suscita sensazioni di odio-amore (qui o qua) e ciò è molto comprensibile a causa della sua complessità e dei suoi limiti. Inoltre non è semplice identificare la casistica in cui utilizzarlo anche a fronte del fatto che, se le specifiche cambiano, questo pattern resta molto faraginoso da mantenere.
Partendo da questa premessa si capisce che non sarà semplice spiegarlo pertanto utilizzerò un approccio incrementale, partendo da un esempio semplice per poi complicarlo. Nella sua forma più semplice questo pattern esprime semplicemente il Double Dispatch che ne costituisce la sua natura che occorrerà rivedere.
Inoltre occorre osservare che questo pattern è stato oggetto di proposte di rivisitazione, ma in questo contesto mi limiterò ad usarlo nella sua forma originaria.

Come esempio, ho pensato a queste implementazioni:

  1. Hello Visitor: semplice esempio che ha lo scopo di introdurre il Double Dispatch
  2. Calcolo dell’aria del rettangolo: 1 elemento ed 1 algoritmo
  3. Calcolo del perimetro del rettangolo: 1 elemento e 2 algoritmi
  4. Calcolo dell’aria e del perimetro del triangolo: 2 elementi e 4 algoritmi
  5. Calcolo dell’aria e del perimetro del triangolo, ciclando una lista: 2 elementi e 4 algoritmi

1. Hello Visitor
Per iniziare riprendo l’esempio fatto all’inizio, il più semplice possibile, al fine di invocare un comportamento, definito in una classe Visitor, su di uno stato, definito in una classe Element. Un semplice “Hello Visitor!”.

Definiamo la classe Element che definisce lo stato (proprietà hello), l’accessor (metodo getHello()) ed il collegamento con il Visitor(metodo accept()):

package patterns.visitor.hello;

public class Element {

    private String hello = "Element";

    public String getHello() {
        return hello;
    }
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

Definiamo la classe Visitor che definisce la modalità di interfacciamento con l’Element (metodo visit()) che, accettando come paramentro l’oggetto Element, sarà in grado di recuperare lo stato dell’Element tramite il suo accessor (metodo getHello())

package patterns.visitor.hello;

public class Visitor {

    public void visit(Element element) {
        System.out.println( "Hello " + element.getHello());
    }

}

Per simulare la loro interazione definiamo la classe Client che si occupa di invocare l’Element passando al metodo accept() il Visitor che definisce il comportamento da adottare.

package patterns.visitor.hello;

public class Client {

    public static void main(String[] args) {
        Element element = new Element();
        element.accept( new Visitor() );
    }

}

L’esecuzione del codice ha come risultato:

$JAVA_HOME/bin/java patterns.visitor.hello.Client
Hello Element

2.Calcolo dell’aria del rettangolo

Passiamo adesso ad un esempio più utile, ma non troppo (altrimenti diventa complicato), ossia come calcolare l’area di un rettangolo.

Vediamo come si presenta il pattern in UML in base all’esempio:

Aria Rettangolo

Aria Rettangolo

Vediamo come si presenta il Sequence Diagram in UML in base all’esempio:

Aria del rettangolo

Definiamo la classe ElementRettangolo che detiene le informazioni dello stato del rettangolo ( ossia altezza e larghezza), i metodi accessor e mutator (ossia i get() e set() ) ed il metodo di accesso e di dialogo con il Visitor (metodo accept())

package patterns.visitor.rettangolo.aria;

public class ElementRettangolo {

    private int altezza;
    private int larghezza;

    public int getAltezza() {
        return this.altezza;
    }

    public void setAltezza(int altezza) {
        this.altezza = altezza;
    }

    public int getLarghezza() {
        return this.larghezza;
    }

    public void setLarghezza(int larghezza) {
        this.larghezza = larghezza;
    }

    public void accept(VisitorRettangoloAria visitor) {
        visitor.visitRettangoloAria(this);
    }
}

Definiamo la classe VisitorRettangoloAria che definisce il metodo visitRettangoloAria() che viene utilizzato per calcolare l’area del rettangolo e che verrà invocato dal metodo accept() dell’element ElementRettangolo.

package patterns.visitor.rettangolo.aria;

public class VisitorRettangoloAria {
    public void visitRettangoloAria(ElementRettangolo elementRettangolo) {
        int aria = elementRettangolo.getAltezza() * elementRettangolo.getLarghezza();
        System.out.println("L'area del rettangolo e': " + aria);
    }
}

Definiamo la classe Client che setta le proprietà del rettangolo, ossia il suo stato, e poi si avvale del Visitor, VisitorRettangoloAria, per calcolare la sua area.

package patterns.visitor.rettangolo.aria;

public class Client {
    public static void main(String[] args) {
        ElementRettangolo elementRettangolo = new ElementRettangolo();
        elementRettangolo.setAltezza(10);
        elementRettangolo.setLarghezza(20);
        elementRettangolo.accept( new VisitorRettangoloAria() );
    }
}

Eseguiamo il nostro Client e verifichiamo.

$JAVA_HOME/bin/java patterns.visitor.rettangolo.aria.Client
L'area del rettangolo e': 200

3.Calcolo del perimetro del rettangolo.
Adesso ampliamo l’esempio fatto precedentemente inserendo una nuova operazione. In questo caso modifichiamo la struttura dell’esempio per avvicinarci sempre più alla struttura del pattern.

Aria e Perimetro Rettangolo

Aria e Perimetro Rettangolo

Vediamo come si presenta il Sequence Diagram in UML in base all’esempio:

Perimetro del rettangolo

Dobbiamo definire una classe padre dei Visitor, che prima non avevamo, al fine di definire tutte le operazioni presenti.
In particolare abbiamo le operazioni relative al calcolo dell’aria e del perimetro del rettangolo.

package patterns.visitor.rettangolo.ariaPerimetro;

abstract class Visitor {

    public abstract void visitRettangoloAria(ElementRettangolo elementRettangolo);

    public abstract void visitRettangoloPerimetro(ElementRettangolo elementoRettangolo);

}

Di seguito dobbiamo relazionare la classe VisitorRettangoloAria con il padre e definire entrambi i metodi. Ovviamente il metodo relativo al calcolo del perimetro non dovrà essere implementato.

package patterns.visitor.rettangolo.ariaPerimetro;

public class VisitorRettangoloAria extends Visitor {

    @Override
    public void visitRettangoloAria(ElementRettangolo elementRettangolo) {
        int aria = elementRettangolo.getAltezza() * elementRettangolo.getLarghezza();
        System.out.println("L'area del rettangolo e': "+ aria);
    }

    @Override
    public void visitRettangoloPerimetro(ElementRettangolo elementoRettangolo) {
        throw new UnsupportedOperationException("Not supported.");
    }

}

Adesso definiamo l’altro Visitor, VisitorRettangoloPerimetro, che si occupa di calcolare il perimentro del rettangolo. Come nel caso precedente, dobbiamo prevedere entrambi i metodi ma dobbiamo implementare solo quello di interesse: visitRettangoloPerimetro.

package patterns.visitor.rettangolo.ariaPerimetro;

public class VisitorRettangoloPerimetro extends Visitor {

    @Override
    public void visitRettangoloAria(ElementRettangolo elementRettangolo) {
        throw new UnsupportedOperationException("Not supported.");
    }

    @Override
    public void visitRettangoloPerimetro(ElementRettangolo elementoRettangolo) {
       int perimentro = (elementoRettangolo.getAltezza() + elementoRettangolo.getLarghezza())*2;
       System.out.println("Il perimetro del rettangolo e': " + perimentro );
    }

}

Adesso nella classe Element, ElementRettangolo, definiamo un comportamento diverso a seconda dell’oggetto passato come paramentro al metodo accept(). Poi vedremo come questa sezione può essere migliorata.

package patterns.visitor.rettangolo.ariaPerimetro;

public class ElementRettangolo {

    private int altezza;
    private int larghezza;

    public int getAltezza() {
        return this.altezza;
    }

    public void setAltezza(int altezza) {
        this.altezza = altezza;
    }

    public int getLarghezza() {
        return this.larghezza;
    }

    public void setLarghezza(int larghezza) {
        this.larghezza = larghezza;
    }

    public void accept(Visitor visitor) {
        if (visitor instanceof VisitorRettangoloAria)
            visitor.visitRettangoloAria(this);
        else if (visitor instanceof VisitorRettangoloPerimetro)
            visitor.visitRettangoloPerimetro(this);
    }

}

Infine nella classe Client possiamo invocare il nostro Client che si occupa di creare il rettangolo e successivamente di invocare le operazioni relative al calcolo dell’aria e del perimetro in base al tipo di Visitor che viene passato.

package patterns.visitor.rettangolo.ariaPerimetro;

public class Client {

    public static void main(String[] args) {
        ElementRettangolo elementRettangolo = new ElementRettangolo();
        elementRettangolo.setAltezza(10);
        elementRettangolo.setLarghezza(20);
        elementRettangolo.accept( new VisitorRettangoloPerimetro() );
        elementRettangolo.accept( new VisitorRettangoloAria() );
    }

}

Vediamo l’output del nostro codice.

$JAVA_HOME/bin/java patterns.visitor.rettangolo.ariaPerimetro.Client
Il perimetro del rettangolo e': 60
L'area del rettangolo e': 200

4.Inserimento Triangolo e calcolo dell’aria e perimetro
In questo esercizio introduciamo un altro elemento, il triangolo.
L’introduzione di un elemento nuovo causa più cambiamenti rispetto all’introduzione di un nuovo algoritmo/operazione.

  • l’introduzione di una nuova operazione comporta la creazione di una nuova classe Visitor che dovrà implementare tutte le operazioni dichiarate nell’interfaccia.
  • l’introduzione di un nuovo elemento comporta l’introduzione di una nuova operazione nell’interfaccia Visitor pertanto la modifica di tutti i Visitor, violando anche il principio Open-Closed.

Vediamo il tutto introducendo un nuovo elemento e nuove operazioni dichiarate nell’interfaccia Visitor.

Vediamo come si presenta il Class Diagram UML

Triangolo e Rettangolo

Triangolo e Rettangolo

Vediamo come si presenta il Sequence Diagram in UML in base all’esempio:

Calcolo dell aria e perimetro

L’ElementTriangoloRettangolo prevede nel proprio metodo accept() la redirezione verso i metodi di calcolo dell’aria o del perimetro. Questo ha fatto si che si sono dovuti inserire 2 nuove operazioni nell’interfaccia Visitor e ciò ha comportato la modifica di tutti i Visitor.

package patterns.visitor.triangolo;

public class ElementTriangoloRettangolo extends Element{

    private int base;
    private int altezza;

    public int getBase() {
        return base;
    }

    public void setBase(int base) {
        this.base = base;
    }

    public int getAltezza() {
        return altezza;
    }

    public void setAltezza(int altezza) {
        this.altezza = altezza;
    }

    @Override
    public void accept(Visitor visitor) {
        if (visitor instanceof VisitorTriangoloRettangoloAria)
            visitor.visitTriangoloRettangoloAria(this);
        else if (visitor instanceof VisitorTriangoloRettangoloPerimetro)
            visitor.visitTriangoloRettangoloPerimetro(this);
    }

}

Vediamo come si presenta l’interfaccia Visitor a seguito dell’inserimento dei metodi invocati dal nuovo Element:

package patterns.visitor.triangolo;

public abstract class Visitor {

    public abstract void visitRettangoloAria(ElementRettangolo elementRettangolo);

    public abstract void visitRettangoloPerimetro(ElementRettangolo elementoRettangolo);

    public abstract void visitTriangoloRettangoloAria(ElementTriangoloRettangolo elementTriangoloRettangolo);

    public abstract void visitTriangoloRettangoloPerimetro(ElementTriangoloRettangolo elementoTriangoloRettangolo);
}

Vediamo l’implementazione dei nuovi Visitor:…il primo Visitor…

package patterns.visitor.triangolo;

public class VisitorTriangoloRettangoloAria extends Visitor {

    @Override
    public void visitRettangoloAria(ElementRettangolo elementRettangolo) {
        throw new UnsupportedOperationException("Not supported.");
    }

    @Override
    public void visitRettangoloPerimetro(ElementRettangolo elementoRettangolo) {
        throw new UnsupportedOperationException("Not supported.");
    }

    @Override
    public void visitTriangoloRettangoloAria(ElementTriangoloRettangolo elementTriangoloRettangolo) {
        int aria = ((elementTriangoloRettangolo.getBase() * elementTriangoloRettangolo.getAltezza())/2);
        System.out.println("L'area del triangolo e': " + aria);
    }

    @Override
    public void visitTriangoloRettangoloPerimetro(ElementTriangoloRettangolo elementoTriangoloRettangolo) {
        throw new UnsupportedOperationException("Not supported.");
    }

}

…ed il secondo Visitor…

package patterns.visitor.triangolo;

public class VisitorTriangoloRettangoloPerimetro extends Visitor {

    @Override
    public void visitRettangoloAria(ElementRettangolo elementRettangolo) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public void visitRettangoloPerimetro(ElementRettangolo elementoRettangolo) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public void visitTriangoloRettangoloAria(ElementTriangoloRettangolo elementTriangoloRettangolo) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public void visitTriangoloRettangoloPerimetro(ElementTriangoloRettangolo elementTriangoloRettangolo) {
        int perimetro = (elementTriangoloRettangolo.getBase() + (elementTriangoloRettangolo.getAltezza()*2));
        System.out.println("Il perimetro del triangolo e': " + perimetro );
    }

}

Il Client di seguito invocherà entrambi gli Element:

package patterns.visitor.triangolo;

public class Client {

    public static void main(String[] args) {
        ElementRettangolo elementRettangolo = new ElementRettangolo();
        elementRettangolo.setAltezza(10);
        elementRettangolo.setLarghezza(20);
        elementRettangolo.accept( new VisitorRettangoloPerimetro() );
        elementRettangolo.accept( new VisitorRettangoloAria() );

        ElementTriangoloRettangolo elementTriangoloRettangolo = new ElementTriangoloRettangolo();
        elementTriangoloRettangolo.setBase(10);
        elementTriangoloRettangolo.setAltezza(20);
        elementTriangoloRettangolo.accept( new VisitorTriangoloRettangoloPerimetro());
        elementTriangoloRettangolo.accept( new VisitorTriangoloRettangoloAria());
    }

}

L’output del Client e’ il seguente:

$JAVA_HOME/bin/java patterns.visitor.triangolo.Client
Il perimetro del rettangolo e': 60
L'area del rettangolo e': 200
Il perimetro del triangolo e': 50
L'area del triangolo e': 100

5.Inserimento di una classe di Aggregazione e calcolo dell’aria e perimetro
In questo esercizio completiamo il pattern inserendo l’ultimo elemento: ObjectStructure che contiene l’elenco degli elemento che dovranno essere invocati
Vediamo come si presenta il Class Diagram UML

Class Completo calcolo dell’aria e perimetro

Vediamo come si presenta il Sequence Diagram UML

Sequence Completo calcolo dell’aria e perimetro

In questo caso la modifica consiste nell’introduzione della classe Struttura che avrà il compito di inizializzare ed invocare gli elementi contenuti nella propria lista. Vediamo meglio:

package patterns.visitor.triangolo;

import java.util.Iterator;
import java.util.Vector;

public class Struttura extends Vector {

    public Struttura(){
        ElementRettangolo elementRettangolo = new ElementRettangolo();
        elementRettangolo.setAltezza(10);
        elementRettangolo.setLarghezza(20);
        add( elementRettangolo);

        ElementTriangoloRettangolo elementTriangoloRettangolo = new ElementTriangoloRettangolo();
        elementTriangoloRettangolo.setBase(10);
        elementTriangoloRettangolo.setAltezza(20);
        add( elementTriangoloRettangolo );
    }

    public void esegui() {
        Iterator iterator = this.iterator();
        while( iterator.hasNext()){
            Element element = (Element) iterator.next();
            element.accept(new VisitorRettangoloAria());
            element.accept(new VisitorRettangoloPerimetro());
            element.accept(new VisitorTriangoloRettangoloAria());
            element.accept(new VisitorTriangoloRettangoloPerimetro());
        }
    }
}

In questo caso il Client si presenta più snello in quanto deve semplicemente invocare la classe Struttura, che per noi funge da Facade:

package patterns.visitor.triangolo;

public class Client {

    public static void main(String[] args) {
        Struttura struttura = new Struttura();
        struttura.esegui();
    }

}

L’output del Client e’ il seguente:

$JAVA_HOME/bin/java patterns.visitor.triangolo.Client
L'area del rettangolo e': 200
Il perimetro del rettangolo e': 60
L'area del triangolo e': 100
Il perimetro del triangolo e': 50
Categorie:GOF Pattern, java
  1. Alessandro
    15 luglio 2013 alle 12:12 AM

    Ciao! Grazie per la spiegazione esauriente!🙂 davvero utile. Sapresti indicarmi qualche fonte per informazioni più approfondite sull’argomento?

    Ps. Nell’esempio ‘1. Hello Visitor’ perchè il metodo getHello() è definito come mutator e non come accessor?

  2. Dennis
    12 gennaio 2014 alle 12:02 PM

    Ciao, grazie per la bellissima spiegazione di questo pattern.
    Ma, come hai fatto ad esempio nell’esercizio 3, una classe abstract che contiene tutti i metodi abstract non sarebbe meglio dichiararla come interface?

  1. No trackbacks yet.
I commenti sono chiusi.
%d blogger cliccano Mi Piace per questo: