Visitor Pattern

V

INTRODUZIONE

Oggi daremo un’occhiata al Visitor pattern. Di tutti i pattern che ho usato finora, Visitor è sicuramente uno dei più interessanti. Tale pattern rappresenta un’operazione da eseguire in una collezione di elementi di una struttura. L’operazione può essere modificata senza alterare le classi degli elementi dove opera.  Si consideri una struttura che contiene un insieme eterogeneo di oggetti, sui quali bisogna applicare la stessa operazione, che però è implementata in modo diverso da ogni classe di oggetto. Un esempio di pratico di questa operazione potrebbe semplicemente essere lo shopping al supermercato, in cui il carrello è il tuo insieme di elementi. Quando arrivi alla cassa, il cassiere funge da visitatore, prendendo la serie di elementi disparati (i tuoi acquisti), alcuni con i prezzi e altri che devono essere pesati, al fine di fornirti un totale.

VISITOR PATTERN COME SOLUZIONE

La soluzione consiste nella creazione di un oggetto (ConcreteVisitor), che è in grado di percorrere la collezione, e di applicare un metodo proprio su ogni oggetto (Element) visitato nella collezione (avendo un riferimento a questi ultimi come parametro). Per agire in questo modo bisogna fare in modo che ogni oggetto della collezione aderisca ad un’interfaccia (Visitable), che consente al ConcreteVisitor di essere “accettato” da parte di ogni Element. Poi il Visitor, analizzando il tipo di oggetto ricevuto, esegue l’invocazione alla particolare operazione che in ogni caso si deve eseguire.

Andiamo adesso ad implementare assieme in java un esempio concreto di utilizzo di Visitor Pattern. Il codice funzionante dell’esempio che vi mostrerò lo trovate nel mio repository Github al seguente LINK.

esempio pratico: italiancoders market

Italiancoders market è supermarket dove si vendono due tipi di oggetti: quelli venduti a peso e quelli venduti a pezzi. Nel primo caso è facile intuire che il prezzo è calcolato come numero di pezzi moltiplicato il costo al chilo, mentre nel secondo caso semplicemente come numeri di pezzi per costo unitario. Per calcolare il totale del prezzo della spesa utilizzeremo il Visitor pattern.

Partiamo con il definire la classe astratta che rappresenterà ogni prodotto venduto nel market il quale avrà sicuramente un codice che lo rappresenta e una descrizione.

public abstract class Item {

    protected String code;

    protected String description;

    //GETTER E SETTER E COSTRUTTORI


}

Ho scelto di definire la classe astratta perché non voglio che sia istanziabile; essa sarà la super classe delle due classi concrete ItemSoldInWeight e ItemSoldInPieces

ItemSoldInWeight.JAVA

ItemSoldInWeight è una sotto classe di Item e ha come attributi il prezzo unitario al kilogrammo e il peso in kilogrammi.

public class ItemSoldInWeight extends Item{

    private Double unitPrice;
    private Double weight;
    //GETTER E SETTER E COSTRUTTORI 

}

ItemSoldInpieces.JAVA

ItemSoldInPieces è una sotto classe di Item e ha come attributi il prezzo unitario al pezzo e il numero di pezzi.

public class ItemSoldInPieces extends Item {

    private Double unitPrice;
    private Integer numberOfPieces;

    //getter e setter e costruttori


}

VISITABLE.JAVA

Andiamo adesso a definire l’interfaccia Visitable, che specifica le operazioni di visita per ogni classe concreta che andrà visitata (nel nostro esempio dobbiamo visitare una lista di ItemSoldWeight e ItemSoldInPieces). La visita di un elemento dovrà ritornare come output un double, che rappresenterà il prezzo totale di quel singolo Item della spesa.

public interface Visitable {
    Double accept( Visitor visitor);
}

implementare visitable negli elementi visitable

Occorre quindi adesso modificare gli oggetti concreti visitabili in modo che essi implementino l’interfaccia Visitable.

public class ItemSoldInPieces extends Item implements Visitable {

    private Double unitPrice;
    private Integer numberOfPieces;


    @Override
    public Double accept(Visitor visitor) {
        return visitor.visit(this);

    }

   ....
  }

public class ItemSoldInWeight extends Item  implements Visitable {

    private Double unitPrice;
    private Double weight;


    @Override
    public Double accept(Visitor visitor) {
        return visitor.visit(this);
    }

...
}

Si noti che in entrambi casi, oltre ai metodi particolari di ogni classe, c’è il metodo accept, dichiarato nell’interfaccia Visitable. Questo metodo soltanto riceve un riferimento ad un Visitor, e chiama la sua operazione di visita inviando se stesso come riferimento.

concrete visitor

Adesso andiamo ad implementare il Concrete visitor del pattern: ovvero il componente in grado di scandire la collezione e i suoi oggetti, implementando l’interfaccia Visitor:

public interface Visitor {
    Double visit (ItemSoldInWeight item);
    Double visit (ItemSoldInPieces item);

}

la quale permette di visitare il singolo componente ovvero, nel nostro esempio, produrre in uscita il costo complessivo del prodotto scelto. La nostra implementazione del concrete visitor sarà la seguente:

public class ShoppingVisitor implements Visitor {
    @Override
    public Double visit(ItemSoldInWeight item) {
        return item.getWeight()*item.getUnitPrice();
    }

    @Override
    public Double visit(ItemSoldInPieces item) {
        return item.getNumberOfPieces()*item.getUnitPrice();
    }
}

Come si evince dal codice sopra abbiamo due differenti logiche del visitor che producono il costo del prodotto.

A questo punto mostriamo con un semplice Main l’utilizzo del visitor pattern appena implementato.

public class Main {

    public static void main(String[] args) {
        List<Visitable> items = new ArrayList<>();

        ItemSoldInPieces p1 = new ItemSoldInPieces("CO1","Cereali",2.30D,2);
        ItemSoldInPieces p2 = new ItemSoldInPieces("CO2","Quaderno",1.10D,1);
        ItemSoldInWeight p3 = new ItemSoldInWeight("CO3","Mele",2.50D,2.0D);

        items.add(p1);
        items.add(p2);
        items.add(p3);

        Double totalCost = calculateCost(items);
        System.out.println("Total cost "+totalCost+" €");

    }

    private static Double calculateCost(List<Visitable> items) {
        Double total = 0.0D;
        Visitor visitor = new ShoppingVisitor();

        for(Visitable item : items){
            total = total + item.accept(visitor);
        }
        return total;
    }
}

In questo esempio abbiamo un carrello contenente :

  • 2  confezione di  cereali i quali costano €2.30 il pezzo;
  • 1  quaderno che costa €1.10 il pezzo;
  • 2 Kg di Mele che costano €2.50/Kg;

Il metodo calculateCost utilizza una istanza del visitor appena implementato, il quale  “visita” la collezione di prodotti del carrello producendo per ciascuno elemento il prezzo e andando a calcolare il totale della spesa. Eseguendo il main viene stampato correttamente a video:

Total cost 10.7 €

Vantaggi del visitor pattern

  • Il vantaggio di questo pattern è che se la logica dell’operazione cambia (ad es. logiche sul numero di pezzo per calcolare lo sconto) allora dobbiamo apportare modifiche solo all’implementazione del visitatore piuttosto che farlo in tutte le classi oggetto.
  • Un altro vantaggio è che l’aggiunta di un nuovo elemento al sistema è semplice, richiederà modifiche solo nell’interfaccia e nell’implementazione del visitatore e le classi di Item concreti esistenti non saranno interessate.

 

Limiti del pattern del visitatore

  • Lo svantaggio del pattern visitator è che dovremmo conoscere il tipo di ritorno dei metodi visit () al momento della progettazione altrimenti dovremo modificare l’interfaccia e tutte le sue implementazioni (nel nostro esempio era noto a priori il tipo di ritorno del metodo visit(), ovvero un double essendo il prezzo un numero non intero).
  • Un altro svantaggio è la verbosità di tale pattern: i ConcreteElement devono implementare una particolare interfaccia per essere visitati  e occorre prevedere nell’interfaccia e implementazione del Visitor il metodo di visita per ciascun tipo.

Alcuni di questi problemi possono essere arginati facendo uso della Reflection. Tramite la Reflection API di Java si ottiene un’implementazione più semplice e flessibile, che dà
soluzione a questi inconvenienti, non essendo necessario costringere i ConcreteElement da visitare a implementare una particolare interfaccia, rendendo innecesaria l’interfaccia Visitable. Per chi è curioso su come è possibile implementare Visitor Pattern tramite Reflection API gli consiglio la lettura del seguente  articolo che trovate  a questo LINK.

conlusioni

Concludiamo l’articolo riassumendo quando usare il Visitor Patter e viceversa quando non è adatto il suo utilizzo.

Consigliamo l’utilizzo di tale pattern quando :

  • Vogliamo operare un tipo simile di operazione su oggetti di diverso tipo raggruppati in gerarchia o collection.
  • Vogliamo separare i comportamenti distinti e non correlati dalla classe del tipo (nel nostro esempio non avevamo le logiche del calcolo del prezzo all’interno dei due tipi concreti di item) in un’altra classe e vogliamo cambiare i comportamenti in modo dinamico senza modificare il codice delle classi del tipo.
  • Abbiamo la gerarchia degli oggetti  nota a priori e con bassa probabilità di modifiche, ma c’è una forte probabilità di aggiunta di nuove operazioni in futuro. Dato che questo pattern ci consente di separare l’operazione dalla struttura dell’oggetto, ora è facile per noi aggiungere nuove operazioni sotto forma di Visitator. Questo funzionerà finché la struttura dell’oggetto rimarrà invariata.

Il Visitor risulta invece poco utilizzabile nelle seguenti condizioni:

  • Visitor Pattern richiede che gli argomenti e il tipo restituito per i metodi  di visita debbano essere noti in fase di progettazione. Quindi questo modello non è adatto alla situazione in cui i tipi vengono modificati di frequente, perché una volta introdotto il nuovo tipo, tutto il visitatore deve essere modificato di conseguenza.
  • Quando i comportamenti sono correlati alla singola istanza dell’oggetto e non all’intera gerarchia. Tali comportamenti non dovrebbero essere implementati tramite il visitator, poiché il visitator viene utilizzato per definire comportamenti (visitator) che verranno applicati all’intera gerarchia.

A proposito di me

Dario Frongillo

Software architect con un forte background in Java, Architettura REST, framework Spring , Database Design,progettazione e sviluppo di SPA e RIA Web application con framework Javascript. Attualmente mi occupo di sviluppo soluzioni software in ambito Banking e Finance: in particolare progettazione e sviluppo di applicativi web based per la realizzazione di sistemi di trading, interfacce con i mercati finanziari e di servizi real time.

Gli articoli più letti

Articoli recenti

Commenti recenti