Visitor (Besökare)
Introduktion
Visitor är ett beteendemönster som låter dig separera algoritmer från de objekt de opererar på. Du kan lägga till nya operationer till en objektstruktur utan att ändra klasserna.
Problem
Du har ett abstrakt syntaxträd (AST) med noder av typerna NumberNode, AddNode, MultiplyNode. Du vill kunna utföra olika operationer på trädet: beräkna resultatet, generera kod, exportera till XML.
Att lägga in varje operation i nodklasserna bryter mot Single Responsibility Principle och kräver ändringar i alla nodklasser varje gång en ny operation läggs till.
Lösning
Visitor extraherar varje operation till en egen visitor-klass. Noderna implementerar en accept(Visitor)-metod som anropar rätt visit()-metod på besökaren — ett mönster kallat double dispatch.
När ska Visitor användas?
- När du behöver utföra många olika operationer på en objektstruktur utan att förorena klasserna.
- När objektstrukturen sällan ändras men nya operationer läggs till ofta.
- För kompilatorer, AST-traversering, dokumentexport och rapportgenerering.
Struktur
| Roll | Ansvar |
|---|---|
| Visitor Interface | Deklarerar en visit()-metod per elementtyp. |
| Concrete Visitor | Implementerar operationen för varje elementtyp. |
| Element Interface | Deklarerar accept(Visitor). |
| Concrete Element | Implementerar accept() med visitor.visit(this). |
Exempel — Matematiskt uttrycksträd (Java)
Scenariot: Ett uttrycksträd som kan beräknas, skrivas ut som infix-notation eller exporteras till LaTeX — utan att ändra nodklasserna.
// ── Element Interface ─────────────────────────────────────
public interface ExpressionNode {
<T> T accept(ExpressionVisitor<T> visitor);
}
// ── Visitor Interface ─────────────────────────────────────
public interface ExpressionVisitor<T> {
T visitNumber(NumberNode node);
T visitAdd(AddNode node);
T visitMultiply(MultiplyNode node);
T visitNegate(NegateNode node);
}
// ── Concrete Elements ─────────────────────────────────────
public class NumberNode implements ExpressionNode {
private final double value;
public NumberNode(double value) { this.value = value; }
public double getValue() { return value; }
@Override
public <T> T accept(ExpressionVisitor<T> visitor) {
return visitor.visitNumber(this);
}
}
public class AddNode implements ExpressionNode {
private final ExpressionNode left;
private final ExpressionNode right;
public AddNode(ExpressionNode left, ExpressionNode right) {
this.left = left;
this.right = right;
}
public ExpressionNode getLeft() { return left; }
public ExpressionNode getRight() { return right; }
@Override
public <T> T accept(ExpressionVisitor<T> visitor) {
return visitor.visitAdd(this);
}
}
public class MultiplyNode implements ExpressionNode {
private final ExpressionNode left;
private final ExpressionNode right;
public MultiplyNode(ExpressionNode left, ExpressionNode right) {
this.left = left;
this.right = right;
}
public ExpressionNode getLeft() { return left; }
public ExpressionNode getRight() { return right; }
@Override
public <T> T accept(ExpressionVisitor<T> visitor) {
return visitor.visitMultiply(this);
}
}
public class NegateNode implements ExpressionNode {
private final ExpressionNode operand;
public NegateNode(ExpressionNode operand) { this.operand = operand; }
public ExpressionNode getOperand() { return operand; }
@Override
public <T> T accept(ExpressionVisitor<T> visitor) {
return visitor.visitNegate(this);
}
}
// ── Concrete Visitors ─────────────────────────────────────
// Beräknar resultatet
public class EvaluatorVisitor implements ExpressionVisitor<Double> {
@Override public Double visitNumber(NumberNode n) { return n.getValue(); }
@Override public Double visitAdd(AddNode n) { return n.getLeft().accept(this) + n.getRight().accept(this); }
@Override public Double visitMultiply(MultiplyNode n) { return n.getLeft().accept(this) * n.getRight().accept(this); }
@Override public Double visitNegate(NegateNode n) { return -n.getOperand().accept(this); }
}
// Genererar infix-notation med parenteser
public class PrintVisitor implements ExpressionVisitor<String> {
@Override public String visitNumber(NumberNode n) {
double v = n.getValue();
return v == (long) v ? String.valueOf((long) v) : String.valueOf(v);
}
@Override public String visitAdd(AddNode n) {
return "(" + n.getLeft().accept(this) + " + " + n.getRight().accept(this) + ")";
}
@Override public String visitMultiply(MultiplyNode n) {
return "(" + n.getLeft().accept(this) + " × " + n.getRight().accept(this) + ")";
}
@Override public String visitNegate(NegateNode n) {
return "(-" + n.getOperand().accept(this) + ")";
}
}
// Exporterar till LaTeX
public class LatexVisitor implements ExpressionVisitor<String> {
@Override public String visitNumber(NumberNode n) {
double v = n.getValue();
return v == (long) v ? String.valueOf((long) v) : String.valueOf(v);
}
@Override public String visitAdd(AddNode n) {
return n.getLeft().accept(this) + " + " + n.getRight().accept(this);
}
@Override public String visitMultiply(MultiplyNode n) {
return n.getLeft().accept(this) + " \\cdot " + n.getRight().accept(this);
}
@Override public String visitNegate(NegateNode n) {
return "-" + n.getOperand().accept(this);
}
}
// ── Klientkod ─────────────────────────────────────────────
public class Application {
public static void main(String[] args) {
// Bygg uttrycket: (3 + 4) × (-2)
ExpressionNode expr = new MultiplyNode(
new AddNode(new NumberNode(3), new NumberNode(4)),
new NegateNode(new NumberNode(2))
);
EvaluatorVisitor evaluator = new EvaluatorVisitor();
PrintVisitor printer = new PrintVisitor();
LatexVisitor latex = new LatexVisitor();
System.out.println("Uttryck: " + expr.accept(printer));
System.out.println("Resultat: " + expr.accept(evaluator));
System.out.println("LaTeX: " + expr.accept(latex));
System.out.println();
// Ett mer komplext uttryck: 2 × 3 + 4 × 5
ExpressionNode complex = new AddNode(
new MultiplyNode(new NumberNode(2), new NumberNode(3)),
new MultiplyNode(new NumberNode(4), new NumberNode(5))
);
System.out.println("Uttryck: " + complex.accept(printer));
System.out.println("Resultat: " + complex.accept(evaluator));
System.out.println("LaTeX: " + complex.accept(latex));
}
}
Output:
Uttryck: ((3 + 4) × (-2))
Resultat: -14.0
LaTeX: 3 + 4 \cdot -2
(2 × 3 + 4 × 5)
Uttryck: ((2 × 3) + (4 × 5))
Resultat: 26.0
LaTeX: 2 \cdot 3 + 4 \cdot 5
Fördelar
- Open/Closed Principle — Lägg till nya operationer (visitors) utan att ändra elementklasserna.
- Single Responsibility Principle — Varje visitor samlar en hel operation på ett ställe.
- Generics
<T>gör att olika visitors kan returnera olika typer.
Nackdelar
- Stängt för nya elementtyper — Lägger du till en ny nod måste du uppdatera alla visitor-gränssnitt och implementationer.
- Double dispatch kan vara svårt att förstå initialt.
Av Victor Hernandez från Bytebase.se