Il modello di visitatore è una soluzione a un problema di progettazione più generale:
Ho una gerarchia di classi diverse. Ogni classe supporta varie operazioni comuni. Vorremmo ora estendere quella gerarchia, senza dover modificare la gerarchia esistente (ad esempio perché è definita da una libreria a cui non è stato assegnato il codice sorgente).
- Possiamo aggiungere più classi, che devono supportare tutte le operazioni richieste.
- Possiamo aggiungere più operazioni, che devono essere supportate da tutte le classi nella gerarchia.
Quando progettiamo una gerarchia, possiamo scegliere quale delle due direzioni di estensione vogliamo semplificare. (Cercare di fare entrambe le cose è chiamato il Problema di espressione ed è estremamente complicato.)
Poiché questo è un po 'teorico, usiamo un esempio: vari tipi di animali. Ogni animale ha un name()
e crea un sound()
.
Codice libreria:
interface Animal {
String name();
String sound();
}
class Dog implements Animal {
public String name() { return "Dog"; }
public String sound() { return "Woof"; }
}
class Cat implements Animal {
public String name() { return "Cat"; }
public String sound() { return "Meow"; }
}
Codice utente:
Animal[] animals = new Animal[] { new Dog(), new Cat() };
for (Animal animal : animals)
System.out.println(animal.name() + " goes " + animal.sound());
Output:
Dog goes Woof
Cat goes Meow
Estensione aggiungendo classi
Possiamo estendere la gerarchia Animal
aggiungendo nuove classi che implementano tutti i metodi richiesti.
Codice utente:
class Bird implements Animal {
public String name() { return "Bird"; }
public String sound() { return "Tweet"; }
}
...
Animal[] animals = new Animal[] { new Dog(), new Cat(), new Bird() };
for (Animal animal : animals)
System.out.println(animal.name() + " goes " + animal.sound());
Output:
Dog says Woof
Cat says Meow
Bird says Tweet
Consentendo la creazione di nuove sottoclassi di Animal
, abbiamo anche reso impossibile aggiungere nuove operazioni alla gerarchia Animal
. Dopotutto, quell'operazione dovrebbe essere implementata per tutte le classi sia dalla libreria che da qualsiasi codice utente - un'impresa impossibile poiché ci possono essere infinitamente molte sottoclassi.
Estensione aggiungendo nuove operazioni
Non possiamo aggiungere nuove operazioni senza modificare le definizioni di tutte le classi. Questo è piuttosto inelegante. Bene, potremmo controllando con instanceof
, ma è piuttosto fragile:
public static String favouriteToy(Animal animal) {
if (animal instanceof Dog)
return "chewing bone";
if (animal instanceof Cat)
return "yarn ball";
throw new UnsupportedOperationException();
}
Questo è problematico perché è facile dimenticare un animale. Come possiamo controllare staticamente che abbiamo coperto tutti i casi?
Il modello di visitatore offre una via d'uscita, purché il progettista della gerarchia di classi originale abbia anticipato la necessità di operazioni aggiuntive. Immagino ora che l'interfaccia originale abbia un metodo acceptVisitor
aggiuntivo:
Codice libreria:
interface Animal {
String name();
String sound();
<R> R acceptVisitor(AnimalVisitor<R> visitor);
}
interface AnimalVisitor<R> {
R visitDog(Dog dog);
R visitCat(Cat cat);
}
class Dog implements Animal {
public String name() { return "Dog"; }
public String sound() { return "Woof"; }
public <R> R acceptVisitor(AnimalVisitor<R> visitor) {
return visitor.visitDog(this);
}
}
class Cat implements Animal {
public String name() { return "Cat"; }
public String sound() { return "Meow"; }
public <R> R acceptVisitor(AnimalVisitor<R> visitor) {
return visitor.visitCat(this);
}
}
Codice utente:
class FavouriteToy implements AnimalVisitor<String> {
public String visitDog(Dog dog) { return "chewing bone"; }
public String visitCat(Cat cat) { return "yarn ball"; }
public static String get(Animal animal) {
FavouriteToy visitor = new FavouriteToy();
return animal.acceptVisitor(visitor);
}
}
...
Animal[] animals = new Animal[] { new Dog(), new Cat() };
for (Animal animal : animals)
System.out.println(animal.name()
+ " goes " + animal.sound()
+ ", plays with " + FavouriteToy.get(animal));
Output:
Dog goes Woof, plays with chewing bone
Cat goes Meow, plays with yarn ball
Ancora una volta, il punto importante è che questo in effetti ci consente di definire nuovi metodi per tutte le classi in alcune gerarchie senza modificare quelle classi. Questo è utile in due casi:
-
Non possiamo modificare la gerarchia originale perché è una libreria esterna.
-
Abbiamo bisogno di molte operazioni non correlate e non vogliamo mettere diverse operazioni nella stessa classe. Ciò è indicato dal principio di responsabilità singola.
Lo svantaggio del pattern Visitor è che abbiamo perso la possibilità di aggiungere nuove sottoclassi di Animal
. Dovremmo aggiungerli anche all'interfaccia AnimalVisitor
e ciò interromperà tutti i visitatori esistenti.
In pratica, lo schema visitatore è spesso usato nei compilatori. Qui, la sintassi di un programma viene solitamente rappresentata tramite una struttura dati ad albero, in cui ogni elemento dell'albero può avere un tipo diverso. Diverse parti del compilatore fanno cose molto diverse con questo albero: una parte mostra il codice. Una parte ottimizza il codice. Un'altra parte compila il codice in un'altra lingua e un altro visitatore potrebbe essere un interprete.
Tutte queste operazioni non correlate possono essere scritte come visitatori, in modo che le definizioni dei vari tipi negli alberi non contengano nient'altro che i campi dati e un po 'di informazione del visitatore.
Esempio di compilatore semplice:
interface Expression { <R> R acceptVisitor(Visitor<R> v); }
interface Visitor<R> {
R visitConstant(Constant c);
R visitVariable(Variable v);
R visitAddition(Addition a);
}
class Constant implements Expression {
public final int value;
public Constant(int value) { this.value = value; }
public <R> R acceptVisitor(Visitor<R> v) { return v.visitConstant(this); }
}
class Variable implements Expression {
public final String name;
public Variable(String name) { this.name = name; }
public <R> R acceptVisitor(Visitor<R> v) { return v.visitVariable(this); }
}
class Addition implements Expression {
public final Expression left;
public final Expression right;
public Addition(Expression left, Expression right) {
this.left = left;
this.right = right;
}
public <R> R acceptVisitor(Visitor<R> v) { return v.visitAddition(this); }
}
Esempio di albero della sintassi:
Expression example = new Addition(
new Variable("x"),
new Addition(new Constant(2), new Constant(3)));
Stampante carina:
class PrettyPrinting implements Visitor<Void> {
private StringBuffer sb = new StringBuffer();
public static String of(Expression e) {
PrettyPrinting pp = new PrettyPrinting();
e.acceptVisitor(pp);
return pp.sb.toString();
}
public Void visitConstant(Constant c) {
sb.append(c.value);
return null;
}
public Void visitVariable(Variable v) {
sb.append(v.name);
return null;
}
public Void visitAddition(Addition add) {
sb.append('(');
add.left.acceptVisitor(this);
sb.append(" + ");
add.right.acceptVisitor(this);
sb.append(')');
return null;
}
}
Ottimizzazione della piegatura costante:
class ConstantFolding implements Visitor<Expression> {
public Expression visitConstant(Constant c) { return c; }
public Expression visitVariable(Variable v) { return v; }
public Expression visitAddition(Addition add) {
Expression left = add.left.acceptVisitor(this);
Expression right = add.right.acceptVisitor(this);
if (left instanceof Constant && right instanceof Constant) {
int leftValue = ((Constant) left).value;
int rightValue = ((Constant) right).value;
return new Constant(leftValue + rightValue);
}
return new Addition(left, right);
}
}
Interprete:
class Interpreter implements Visitor<Integer> {
private final Map<String, Integer> env;
private Interpreter(Map<String, Integer> env) { this.env = env; }
public static int eval(Expression e, Map<String, Integer> env) {
return e.acceptVisitor(new Interpreter(env));
}
public Integer visitConstant(Constant c) { return c.value; }
public Integer visitVariable(Variable v) { return env.get(v.name); }
public Integer visitAddition(Addition add) {
return add.left.acceptVisitor(this) + add.right.acceptVisitor(this);
}
}
Programma di esempio:
System.out.println("Expression is: " + PrettyPrinting.of(example));
example = example.acceptVisitor(new ConstantFolding());
System.out.println("Optimized expression is: " + PrettyPrinting.of(example));
Map<String, Integer> variables = new HashMap<>();
variables.put("x", 37);
int result = Interpreter.eval(example, variables);
System.out.println("result is: " + result);
Output:
Expression is: (x + (2 + 3))
Optimized expression is: (x + 5)
result is: 42