Pattern in Java

Pattern di creazione

Questi pattern permettono di descrivere come gli oggetti vengono creati. L'idea è di astrarre il modo in cui sono creati gli oggetti per dover fare esplicitamente new il meno possibile.

Factory

Permette di coordinare dove si fa la scelta della creazione di oggetti di una class.

Per esempio, ci sono due versioni B e C di una class A:

abstract class A {
    public abstract String getVal(); 
}

class B extends A {
    private String val;
    B(String val) {
        this.val = val;
    }
    public String getVal() {
        return "B: " + val;
    }
}

class C extends A {
    private String val;
    C(String val) {
        this.val = val;
    }
    public String getVal() {
        return "C: " + val;
    }
}
La fattoria permette di astrarre come si fa la scelta fra B e C:
class AFactory {
    
    public static final int MAX_LENGTH = 3;
    
    public AFactory() {
    }
    
    public static boolean test(String s) {
        return s.length() < MAX_LENGTH;
    }
    
    public static A get(String s) {
        if (test(s)) {
            return new B(s);
        } 
        return new C(s);
    }
}
Adesso si creano gli oggetti di tipo A attraverso Factory
 A a = AFactory.get("ab");
 A b = AFactory.get("abc");
È lo stesso codice, ma adesso a.getVal() restituisce "B: ab", invece b.getVal() restituisce "C: abc".

Beneficio

Abbiamo astratto la creazione di un oggetto di tipo A. Se si vuole cambiare come sono creati gli oggetti di tipo A bisogna cambiare solamente la class AFactory.

Abstract Factory

Una fattoria astratta può essere vista come una fattoria di fattoria. Permette di proporre dei gruppi d'insieme diversi. Un esempio tipico è una applicazione che deve funzionare sia in modo testuale che in modo grafico. Il compito della fattoria astratta sarà di proporre a seconda delle necessità l'insieme degli oggetti grafici o l'insieme degli oggetti testuali equivalenti.

Per esempio, date due versioni delle classe A e B:

abstract class A {
}

class A1 extends A {
    private String val;
    A1(String val) {
        this.val = val;
    }
}

class A2 extends A {
    private String val;
    A2(String val) {
        this.val = val;
    }
}

abstract class B {
}

class B1 extends B {
    private int val;
    B1(int val) {
        this.val = val;
    }
}

class B2 extends B {
    private int val;
    B2(int val) {
        this.val = val;
    }
}
una fattoria ha il compito di dare un insieme compatibile di A e B:
abstract class AbAbstractFactory {
    public abstract A getA(String val);
    public abstract B getB(int i);
}
Nel nostro caso A1 corrisponde a B1 e A2 a B2. Dunque ci sono due fattorie:
class AbAbstractFactory1 extends AbAbstractFactory {
    
    public A getA(String val) {
        return new A1(val);
    }
    
    public B getB(int i) {
        return new B1(i);
    }
}
e
class AbAbstractFactory2 extends AbAbstractFactory {
    
    public A getA(String val) {
        return new A2(val);
    }
    
    public B getB(int i) {
        return new B2(i);
    }
}
Un esempio di utilizzo di queste fattorie è il seguente:
AbAbstractFactory f1 = new AbAbstractFactory1();
AbAbstractFactory f2 = new AbAbstractFactory2();  
A a1 = f1.getA("ab");    // crea un oggetto di tipo A1 
B b1 = f1.getB(1);       // crea un oggetto di tipo B1
A a2 = f2.getA("ab");    // crea un oggetto di tipo A2
B b2 = f2.getB(2);       // crea un oggetto di tipo B2

Singleton

Il pattern singleton permette di assicurare che c'è solamente un'istanza di un oggetto creato. Il modo più semplice di implementare questo pattern in Java è di definire una class final con tutti i metodi statici. Esempi di tale class in Java sono
public final class System {
     ...
}
oppure
public final class Math {
     ...
}
Un modo più sofisticato è di mettere il costruttore della class privato e di avere un metodo statico che è chiamato per creare oggetti. Tale metodo restituisce null quando la creazione è impossibile. Per esempio, abbiamo
class ASingleton {
    private static boolean flag = false;
    
    private ASingleton() {
    }
    
    public static ASingleton get() {
        if (flag) {
            return null;
        }
        flag = true;
        return new ASingleton();
    }
    
    public void finalize() { 
        // questo è dal garbagge collector quando 
        // l'oggetto non è piú usato
        flag = true;
    }
}

Builder

Il builder è una variante della fattoria in cui l'oggetto da restituire è composite. Dunque la chiamata di un Builder può implicare la creazione di diversi oggetti. Il caso tipico è quello di un'applicazione con una interfaccia grafica. In tale caso c'è sempre un builder che deve incaricarsi della costruzione dell'interfaccia.

Per esempio, per costruire un oggetto della class A bisogna dare un oggetto di tipo B:

class A {
    private B b;
    A(B b) {
        this.b = b;
    }
}
Per B ci sono due possibilità:
class B {
    private C c;
    B(C c) {
        this.c = c;
    }
}

class B1 extends B {
    private B b;
    B1(B b, C c) {
        super(c);
        this.b = b;
    }   
}
dove la class C è definita come:
class C {
    C() {
    }
}
Un builder per la class A elabora l'oggetto A in funzione di un parametro.
public class ABuilder {
    public static final int NORMAL = 0;
    public static final int EXTRA = 1;
    public ABuilder() {
    }
    A get(int type) {
        switch (type) {
            case NORMAL:
                return new A(new B(new C()));
            case EXTRA:
                return new A(new B1(new B(new C()), new C()));
            default:
                return null;
        }
    }
}

Prototype

Il pattern prototipo si applica quando la creazione di un oggetto di tipo A è costosa in termini di computazione ma può essere semplificata avendo già un oggetto di tipo A. In Java, nella class Object è previsto un metodo clone
class Object {
   ...
   protected Object clone() throws CloneNotSupportedException {
     if (! (this instanceof Cloneable)) {
       throw new CloneNotSupportedException();
     }
     ....
   }
}
Per essere duplicato un oggetto deve implementare l'interfaccia Cloneable (che è vuota!). Il comportamento di default è di copiare l'oggetto ma non i campi.

Per esempio, per permettere di copiare una class A fuori di A bisogna fare un overriding del metodo clone:

class A implements Cloneable {
    private int i;
    int getI() {
        return i;
    }
    void setI(int i) {
        this.i = i;
    }
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
Adesso si può creare un prototipo:
class APrototype {
    A get(A a, int i) {
        try {
          A res = (A) a.clone();
          res.setI(i);
          return res;
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }
}

Pattern di struttura

I pattern di struttura si applicano per descrivere come è stata organizzata la struttura dei diversi oggetti.

Questa organizzazione può essere fatta usando l'ereditarietà (si parla allora di pattern di class) o usando oggetti che contengono altri oggetti (si parla allora di pattern di oggetti).

Il consiglio è di preferire sempre la seconda soluzione.

Adapter

Il pattern di adattazione si applica quando c'è bisogno di adattare il comportamento di un oggetto B in modo tale che B propone la stessa interfaccia di un altro oggetto A.

Per esempio, abbiamo costruito un'applicazione che permette di usare un oggetto di tipo A:

class A {
   public int getX() {
      ...
   }
   public int getY() {
      ...
   }
}
e vogliamo che si possa usare anche un oggetto B che con qualche modifica può fare tutto quello che fa A.
class B {
   int getXPlusY() {
      ...
   }
   int getXMinuxY() {
      ...
   }
}
Il primo passo è di rappresentare quello che può fare A come un'interfaccia:
interface ACapable {
  int getX();
  int getY();
}
e cambiare A in conseguenza:
class A implements ACapable {
   ....
}
Adesso esistono due possibilità:

Class Adapter

Si può usare l'ereditarietà per adattare B:
class AClassAdapter extends B implements ACapable {
  public int getX() {
     return (getXPlusY() + getXMinusY()) / 2;
  }
  public int getY() {
     return (getXPlusY() - getXMinusY()) / 2;
  }
}

Object Adapter

Si può creare un nuovo oggetto che contiene l'oggetto B:
class AObjectAdapter implements ACapable {
  private B b;
  public int getX() {
     return (b.getXPlusY() + b.getXMinusY()) / 2;
  }
  public int getY() {
     return (b.getXPlusY() - b.getXMinusY()) / 2;
  }
}
Quest'ultima soluzione è da preferire.

Bridge

Il bridge pattern permette di separare l'interfaccia di una class (che cosa si può fare con la class) dalla sua implementazione (come si fa). In tal modo si può usare l'ereditarietà per fare evolvere l'interfaccia o l'implementazione in modo separato.

Per esempio, data l'implementazione

abstract class AImpl {
    abstract void run(String command);
}

abstract class AImpl1 extends AImpl {
    void run(String command) {
    }
}

abstract class AImpl2 extends AImpl {
    void run(String command) {
    }
}

class ABridge {
    static AImpl impl;
    static void setImpl(AImpl impl) {
        ABridge.impl = impl;
    }
    static AImpl getImpl() {
        return impl;
    }
}
si può costruire un semplice singleton che dà accesso a queste implementazioni:
class ABridge {
    static AImpl impl;
    static void setImpl(AImpl impl) {
        ABridge.impl = impl;
    }
    static AImpl getImpl() {
        return impl;
    }
}
Adesso il servizio che è descritto nella class A si serve del singleton per ottenere la sua implementazione e fare delle variazioni.
abstract class A {
    AImpl impl;
    A() {
        impl = ABridge.getImpl();
    }
    abstract void doIt();
}

class A1 extends A {
    void doIt() {
        impl.run("do");
    }
}

class A2 extends A1 {
    void doIt() {
        System.out.println("Trying");
        super.doIt();
        System.out.println("Done");
    }
}

Composite

Il composite pattern è applicato quando c'è bisogno di rappresentare un insieme di oggetti che rappresentano una gerarchia.

Una class astratta contiene tutto quello che si può fare con gli oggetti

abstract class AComposite {
    abstract String getName();
    abstract AComposite[] getSons();
}
Dopo si possono creare degli oggetti semplici:
class ALeaf extends AComposite {
    String getName() {
        return "Leaf";
    }
    AComposite[] getSons() {
        return new AComposite[0];
    }
}
o degli oggetti compositi:
class ANode  extends AComposite {
    private AComposite[] as;
    String getName() {
        return "Node";
    }
    AComposite[] getSons() {
        return as;
    }
}

Decorator

Il decorator pattern è applicato quando si vuole aggiungere una funzionalità ad una class e anche a tutte le class che la estendono.

Per esempio, data una class che può fare un'azione:

abstract class A {
    abstract void doIt();
}
si può creare una class che decora un oggetto di tipo A permettendo di avere una traccia:
class ADecorator extends A {
    private A a;
    void doIt() {
        System.out.println("Trying");
        a.doIt();
        System.out.println("Do it");
    }
}

Facade

Una facade permette di concentrare in una sola class un sistema composto di diverse class.

Per esempio, abbiamo un sistema che contiene una class che permette di fare un'azione:

class B {
    void readIt() {}
}
ed un'altra class che permette di fare un'altra azione:
class C {
    void playIt() {}
}
Una facade permette di concentrare in una sola class l'insieme delle due azioni:
class AFacade {
    B b;
    C c;
    
    void readIt() {
        b.readIt();
    }
    
    void playIt() {
        c.playIt();
    }
    
}

Flyweight

Il flyweight pattern permette di separare la parte variable di una classe dalla parte che può essere riutilizzata, in modo tale da poter avere quest'ultima condivisa fra istanze differenti della parte variabile.

Per esempio, sia data una class A che è molto usata, in cui i campi x e y variano molto da un'istanza ad un'altra, mentre c'è poca variazione sul valore del campo b:

class A {
    private int x;
    private int y;
    private B b;
    public String toString() {
        return b + "(" + x + ", " + y + ")";
    }
}
Si può creare un flyweight che contiene la parte che si può riutilizzare:
class AFlyWeight {
    private B b;
    public String toString(int x, int y) {
        return b + "(" + x + ", " + y + ")";
    }
}
Invece la parte variabile resta nella class A
class A {
    private int x;
    private int y;
    private AFlyWeight aF;
    public String toString() {
        return aF.toString(x, y);
    }
}

Proxy

Una class proxy AProxy è una class che gioca il ruolo di un'altra class A. AProxy ha la stessa interfaccia di A. Ci sono diverse situazioni in cui questo può essere interessante: Per esempio, data una class A che può fare un'azione:
class A {
    A() {}
    void doIt() {
    }
}
si può creare un proxy che crea A solamente quando c'è bisogno di effettuare tale azione:
class AProxy {
    private A a;
    AProxy() {
        a = null;
    }
    
    void doIt() {
        if (a == null) {
            a = new A();
        }
        a.doIt();
    }
}

Pattern di comportamento

Chain of responsability

Il pattern della catena di responsabilità si applica quando c'è una catena di oggetti che possono rispondere ad una richiesta. Questa catena è gerarchica. La richiesta si muove lungo la catena per trovare l'oggetto più adatto per rispondere. Avere una struttura di catena permette che un oggetto non abbia bisogno di conoscere tutti gli elementi ma solamente l'elemento successivo nella catena.

Tipicamente questo pattern è implementato in Java usando un'interfaccia:

interface  Chain {
    void setUp(Chain chain);
    void process(Object o);
    boolean test(Object o);
    void action(Object o);
}
Il primo metodo permette di settare l'elemento successivo. Il secondo metodo è il metodo che tratta una richiesta. I due metodi finali sono dei metodi di appoggio. Il primo permette di verificare se la richiesta possa essere trattata dall'oggetto e nel caso positivo il secondo definisce che cosa fare.

Adesso tutti gli oggetti che possono comparire nella catena hanno bisogno di implementare questa interfaccia. Per esempio, per una class A abbiamo:

public class A implements Chain {
    private Chain chain;
    public void setUp(Chain chain) {
        this.chain = chain;
    }
    public void process (Object o) {
        if (test(o)) {
            action(o);
        } else {
            if (chain != null) {
                chain.process(o);
            }
        }
    }
    public boolean test(Object o) {
        ...
    }
    public void action(Object o) {
        ...
    }
    ...
}
Per un'altra class B:
class B implements Chain {
    private Chain chain;
    public void setUp(Chain chain) {
        this.chain = chain;
    }
    public void process (Object o) {
        if (test(o)) {
            action(o);
        } else {
            if (chain != null) {
                chain.process(o);
            }
        }
    }
    public boolean test(Object o) {
       ....
    }
    public void action(Object o) {
       ...
    }
    ...
}
Tutte le class che implementano l'interfaccia hanno lo stesso codice per process. Questa è una situazione in cui il fatto di non avere la multi-ereditarietà obbliga a duplicare codice.

Command

Il pattern command permette di astrarre la chiamata di un metodo.

In Java, un command prende la forma di un'intefaccia:

interface Command {
    void execute();
}
Adesso data la class A
class A { 
    ...
    public void doIt() {
       ...
    }
    ...
}
per trasformare la chiamata del metodo doIt in un command, si fa:
class ACommand implements Command {
    A a;
    ACommand(A a) {
        this.a = a;
    }
    public void execute() {
        a.doIt();
    }
}
Adesso nella class B si può permettere di eseguire il metodo doIt senza conoscere A:
class B {
    Command c;
    void setCommand(Command c) {
        this.c = c;
    }
    void run() {
        c.execute();
    }
}

Interpreter

Il pattern di interprete si applica nella situazione in cui è utile avere un piccolo linguaggio di comandi o di macro. Nei casi semplici si può scrivere il parser e l'interprete direttamente usando class comme StringTokenizer. Nei casi più elaborati si usa un toolkit dedicato come ANTLR.

Iterator

Il pattern d'iterazione permette di fare un'iterazione su tutti gli elementi di una struttura senza conoscere l'esatta implementazione della struttura.

In Java è rappresentato dall'interfaccia Enumeration.

public interface Enumeration {
    boolean hasMoreElements();
    Object nextElement();
}

Per esempio, un metodo che itera su un parametro di tipo Enumeration che contiene degli elementi di tipo A si scrive come
     void process(Enumeration enum) {
       while (enum.hasMoreElements()) {
         A a = (A) enum.nextElement();
         ...
       }
     }
Ogni struttura dati in Java (Vector, Hashtable, ...) ha un metodo elements() che permette di ottenere un'enumerazione. Si può anche creare una propria enumerazione. Per esempio, si può creare una class che trasforma un array in una enumerazione:
import java.util.Enumeration;
import java.util.NoSuchElementException;

class ArrayEnumeration implements Enumeration {
    private int index;
    private Object[] array;
    ArrayEnumeration(Object[] array) {
        this.array = array;
        index = 0;
    }
    public Object nextElement() {
        if (array.length <= index) {
            throw new NoSuchElementException();
        }
        return array[index++];
    }
    public boolean hasMoreElements() {
        return (index < array.length);
    }
}
Si può anche modificare gli iteratori. Per esempio, data l'interfaccia che permette di selezionare gli oggetti validi
interface Filter {
    boolean valid(Object o);
}
si può trasformare un'enumerazione di oggetti in un'enumerazione di oggetti validi:
class FilteredEnumeration implements Enumeration {
    private Enumeration enum;
    private Filter filter;
    private Object element;
    private boolean flag;
    FilteredEnumeration(Enumeration enum, Filter filter) {
        this.enum = enum;
        this.filter = filter;
        element = null;
        flag = false;
    }
    
    public Object nextElement() {
        if (flag && filter.valid(element)) {
            flag = false;
            return element;
        }
        Object res;
        while (true) {
            res = enum.nextElement();
            if (filter.valid(res)) {
                return res;
            }
        }
    }
    public boolean hasMoreElements() {
        while (true) {
            if (!(enum.hasMoreElements())) {
                return false;
            }
            element  = enum.nextElement();
            if (filter.valid(element)) {
                flag = true;
                return true;
            }
        }
    }
        
}

Mediator

Il pattern di mediazione si applica in un gruppo di oggetti che interagiscono fra di loro. In tal caso si può concentrare in un oggetto tutte le richieste di interazione. Così facendo gli oggetti del gruppo devono solamente conoscere il mediatore per interagire fra di loro.

Per esempio, consideriamo un gruppo di 3 oggetti, uno di tipo A:

class A {
    private B  b;
    private C c;
    void doIt() {
        b.reset();
        c.doIt();
    }
    void print(String s) {
        ....
    }
    
}
uno di tipo B:
class B {
    private A a;
    void reset() {
       ....
    }
    void print(String s) {
        a.print(s);
    }
}
e uno di tipo C:
class C {
    private A a;
    void doIt() {
       ....
    }
    void print(String s) {
        a.print(s);
}
Applicando il pattern di mediazione, si crea un oggetto che conosce i tre oggetti:
class AMediator {
    private A a;
    private B b;
    private C c;
    
    void doIt() {
        b.reset();
        c.doIt();
    }
    
    void print(String s) {
        a.print(s);
    }
}
Le class A, B e C si modificano di conseguenza:
class A {
    private AMediator mediator;
    void doIt() {
        mediator.doIt();
    }
    void print(String s) {
    }
}  
class B {
    private AMediator mediator;
    void reset() {
    }
    void print(String s) {
        mediator.print(s);
    }
}
class C {
    private AMediator mediator;
    void doIt() {
    }
    void print(String s) {
        mediator.print(s);
    }
}

Memento

Il pattern di memorizzazione si applica quando c'è bisogno di conservare lo stato di un oggetto per dare la possibilità di recuperare questo stato più avanti.

Per esempio, dato un oggetto di tipo A dove il suo stato è rappresentato da un intero e da una stringa:

class A {
    private int value;
    private String name;
    int getValue() {
        return value;
    }
    void setValue(int value) {
        this.value = value;
    }
    String getName() {
        return name;
    }
    void setName(String name) {
        this.name = name;
    }    
    ....
}
si può conservare il suo stato usando un oggetto di tipo AMemento
    
class AMemento {
    private int value;
    private String name;
    private A a;
    
    AMemento(A a) {
        this.a = a;
        value = a.getValue();
        name = a.getName();
    }
    
    void reset() {
        a.setValue(value);
        a.setName(name);
    }
}
Quando occorre, si può restituire lo stato dell'oggetto a memorizzato chiamando il metodo reset.

Observer

Il pattern di osservazione si applica quando ci sono diversi oggetti interessati alle modifiche di un oggetto. Un'applicazione tipica di questo pattern è nella visualizzazione di un oggetto. Usare il pattern di osservazione permette di separare l'oggetto che è visualizzato dalla visualizzazione.

Per esempio, considerando un oggetto A che ha una valore x:

class A {
    private int x;
    int getX() {
        return x;
    }
    void setX(int x) {
        this.x = x;
    }
    ...
}
Per permettere di osservare le modifiche sul valore x, si crea prima un'xinterfaccia che rappresenta gli osservatori:
interface AObserver {
    void update();    
}
Dopo si può modificare la class A per tenere conto dei suoi osservatori:

class A {
    private int x;
    private AObserver[] observers;
    int getX() {
        return x;
    }
    void setX(int x) {
        this.x = x;
        for (int i = 0; i < observers.length; i++) {
            observers[i].update();
        }
    }
    void addAObserver(AObserver o) {
        AObserver[] newObservers = new AObserver[observers.length + 1];
        System.arraycopy(observers, 0, newObservers, 0, observers.length);
        newObservers[observers.length] = o;
        observers = newObservers;
    }
    ...
}
Un osservatore ha bisogno di registrarsi con il metodo addAObserver e dopo viene avvertito con il metodo update quando il valore è stato cambiato.

State

Il pattern di stato si applica quando c'è un oggetto che cambia stato. Invece di avere un metodo che effettua dei test per sapere quale codice usare, si usano gli oggetti.

Per esempio, dato il codice seguente:

class A {
    private int state;
    public static final int A1 = 1;
    public static final int A2 = 2;
    void execute() {
        switch (state) {
            case A1: {
                 ...
                 state = A2;
                 return;
            }
            case A2: {
                ...
                state = A1;
                return;
            }
        }
    }
}
dove gli stati A1 e A2 si alternano, applicando il pattern di state, si crea prima la class degli stati
abstract class AState {
    abstract void execute(A a);
}
L'argomento a nel metodo execute permette di cambiare lo stato. Adesso il codice di A diventa:
class A {
    private AState state;
    void setState(AState state) {
        this.state = state;
    }
    
    void execute() {
        state.execute(this);
    }
}
La class A delega l'esecuzione al suo stato. Per completare l'implementazione e ottenere il precedente alternarsi tra A1 e A2, abbiamo:
class A1 extends AState {
    void execute(A a) {
        ...
        a.setState(new A2());
    }
}
class A2 extends AState {
    void execute(A a) {
        ...
        a.setState(new A1());
    }
}

Strategy

Il pattern di strategia è molto simile a quello di stato. Il pattern di strategia permette di avere dei comportamenti a scelta. Si applica in situazioni dove esistono diverse strategie per fare una cosa ma l'utente (o l'oggetto) ne sceglie una.

Per esempio, per implementare una class A che stampa un oggetto in due modi diversi (postscript o svg) si può scrivere come segue:

abstract class AStrategy {
    abstract void print(Object o);
}
class APs extends AStrategy {
    void print(Object o) {
        ...
    }
}
class ASvg extends AStrategy {
    void print(Object o) {
        ...
    }
}
class A {
    private AStrategy strategy;
    void setStrategy(AStrategy strategy) {
        this.strategy = strategy;
    }
    void print(Object o, String s) {
        if ("ps".equals(s)) {
            setStrategy(new APs());
        } else if ("svg".equals(s)) {
            setStrategy(new ASvg());
        }
        print(o);
    }
    void print(Object o) {
        strategy.print(o);
    }
}

Template

Il pattern di template permette di scrivere metodi generici. Tale pattern è la base della programmazione orientata agli oggetti. Per istanziare questo metodo si usa l'ereditarietà dando un'implementazione alle operazioni sulle quali si appoggia il metodo generico.

Per esempio, consideriamo un metodo generico per disegnare un quadrato. Se si dispone di un metodo per andare avanti e di un metodo per girare a destra, un metodo generico può essere definito andando avanti, girando, avanti, girando, avanti. Tale metodo generico si scrive in Java nel modo seguente:

abstract class A {
    abstract void goForth();
    abstract void goRight();
    final void doSquare() {
        goForth();
        goRight();
        goForth();
        goRight();
        goForth();
        goRight();
        goForth();
    }   
}
Adesso nelle class che derivano da A ogni volta che si darà un'implementazione di goForth e goRight si erediterà un metodo per fare dei quadrati. La cosa importante è che se il codice di doSquare è stato scritto prima dell'implementazione di goForth e goRight si userà comunque tale implementazione nell'esecuzione.

Visitor

Il pattern di visita si applica quando si ha un insieme di oggetti e c'è bisogno di "visitare" questi oggetti per effettuare la computazione. L'idea del pattern di visita è che questa visita si può fare in modo esterno all'oggetto, occorre solamente che l'oggetto preveda di essere visitato.

Tale pattern è molto utile per esempio nel caso di un oggetto composito. Per esempio, consideriamo gli alberi binari. Abbiamo una class astratta

abstract class Tree { 
}
e due sottoclass
class Node extends Tree {
    private String name;
    private Tree left;
    private Tree right;
    Node(String name, Tree left, Tree right) {
        this.right = right;
        this.left = left;
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public Tree getLeft() {
        return left;
    }
    public Tree getRight() {
        return right;
    }
}
class Leaf extends Tree {
    private int value;
    Leaf (int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }
}
Adesso un visitatore per questa class sarà un oggetto che ha due metodi, uno per ogni sottoclass:
abstract class Visitor {
    abstract void visit(Node node);
    abstract void visit(Leaf leaf);
}
La prima cosa da fare è di permettere a ogni componente di un albero di accettare il visitatore. Si fa con il metodo accept:
abstract class Tree { 
    abstract void accept(Visitor v);
}
class Node extends Tree {
    private String name;
    private Tree left;
    private Tree right;
    Node(String name, Tree left, Tree right) {
        this.right = right;
        this.left = left;
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public Tree getLeft() {
        return left;
    }
    public Tree getRight() {
        return right;
    }
    public void accept(Visitor v) {
        v.visit(this);
    }
}
class Leaf extends Tree {
    private int value;
    Leaf (int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }
    public void accept(Visitor v) {
        v.visit(this);
    }    
}
Adesso per scrivere un visitatore si può estendere la class Visitor. Per esempio, possiamo scrivere un visitatore che stampa gli elementi dell'albero in modo prefisso:
class PrefixVisitor extends Visitor {
    void visit(Node node) {
        System.out.println(node.getName());
        node.getLeft().accept(this);
        node.getRight().accept(this);
    }
    void visit(Leaf leaf) {
        System.out.println(leaf.getValue());
    }
    
}
un altro in modo postfisso:
class PostfixVisitor extends Visitor {
    void visit(Node node) {
        node.getLeft().accept(this);
        node.getRight().accept(this);
        System.out.println(node.getName());

    }
    void visit(Leaf leaf) {
        System.out.println(leaf.getValue());
    }
    
}
Dato l'albero
Tree t = new Node("a", new Node("b", new Leaf(1), new Leaf(2)), new Leaf(3));
t.accept(new PrefixVisitor());
a
b
1
2
3
Invece
t.accept(new PostfixVisitor());
1
2
b
3
a
Usando il pattern di visita si possono aggiungere funzionalità ad una class fuori della sua definizione. Il fatto che il meccanismo di visita sia così complicato (accept chiama visit che richiama accept) è una conseguenza del fatto che l'overloading è risolto staticamente.

Laurent Théry
Last modified: Tue Jun 1 13:35:40 MEST 2004