Card 03/ 09
Visitor Template in Python, Java and TypeScript
Two element classes, two visitors, and accept() on each element.
The Visitor template is the pattern in its pure form, with the participant names from the class diagram. Two element classes, Circle and Square, are visited by two operations: one sums their areas, the other exports them as SVG tags. The steps build the Python version, and the tabs give the complete program in three languages, each with its output.
Building the template one part at a time
Visitor
One method per element class.
class Visitor(ABC):
@abstractmethod
def visit_circle(self, circle: Circle) -> None: ...
@abstractmethod
def visit_square(self, square: Square) -> None: ...Concrete Element
accept calls the method for its own class.
def accept(self, visitor: Visitor) -> None:
visitor.visit_circle(self)Concrete Visitor
One operation, with a case for each element.
def visit_circle(self, circle: Circle) -> None:
self.total += math.pi * circle.radius**2Client
Walks the elements and passes the visitor to each.
for shape in shapes:
shape.accept(area)The complete template
from __future__ import annotations
import math
from abc import ABC, abstractmethod
class Visitor(ABC):
@abstractmethod
def visit_circle(self, circle: Circle) -> None: ...
@abstractmethod
def visit_square(self, square: Square) -> None: ...
class Element(ABC):
@abstractmethod
def accept(self, visitor: Visitor) -> None: ...
class Circle(Element):
def __init__(self, radius: float) -> None:
self.radius = radius
def accept(self, visitor: Visitor) -> None:
visitor.visit_circle(self)
class Square(Element):
def __init__(self, side: float) -> None:
self.side = side
def accept(self, visitor: Visitor) -> None:
visitor.visit_square(self)
class AreaVisitor(Visitor):
def __init__(self) -> None:
self.total = 0.0
def visit_circle(self, circle: Circle) -> None:
self.total += math.pi * circle.radius**2
def visit_square(self, square: Square) -> None:
self.total += square.side**2
class ExportVisitor(Visitor):
def __init__(self) -> None:
self.lines: list[str] = []
def visit_circle(self, circle: Circle) -> None:
self.lines.append(f"<circle r='{circle.radius}'/>")
def visit_square(self, square: Square) -> None:
self.lines.append(f"<rect width='{square.side}'/>")
shapes: list[Element] = [Circle(1), Square(2), Circle(0.5)]
area = AreaVisitor()
for shape in shapes:
shape.accept(area)
print(f"total area: {area.total:.2f}")
export = ExportVisitor()
for shape in shapes:
shape.accept(export)
print("\n".join(export.lines))total area: 7.93
<circle r='1'/>
<rect width='2'/>
<circle r='0.5'/>import java.util.ArrayList;
import java.util.List;
public class ViTemplate {
public static void main(String[] args) {
List<Element> shapes = List.of(new Circle(1), new Square(2), new Circle(0.5));
AreaVisitor area = new AreaVisitor();
for (Element shape : shapes) {
shape.accept(area);
}
System.out.printf("total area: %.2f%n", area.total);
ExportVisitor export = new ExportVisitor();
for (Element shape : shapes) {
shape.accept(export);
}
System.out.println(String.join("\n", export.lines));
}
}
interface Visitor {
void visit(Circle circle);
void visit(Square square);
}
interface Element {
void accept(Visitor visitor);
}
record Circle(double radius) implements Element {
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
record Square(double side) implements Element {
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
class AreaVisitor implements Visitor {
double total = 0;
public void visit(Circle circle) {
total += Math.PI * circle.radius() * circle.radius();
}
public void visit(Square square) {
total += square.side() * square.side();
}
}
class ExportVisitor implements Visitor {
final List<String> lines = new ArrayList<>();
public void visit(Circle circle) {
lines.add("<circle r='" + circle.radius() + "'/>");
}
public void visit(Square square) {
lines.add("<rect width='" + square.side() + "'/>");
}
}total area: 7.93
<circle r='1.0'/>
<rect width='2.0'/>
<circle r='0.5'/>interface Visitor {
visitCircle(circle: Circle): void;
visitSquare(square: Square): void;
}
interface Element {
accept(visitor: Visitor): void;
}
class Circle implements Element {
constructor(readonly radius: number) {}
accept(visitor: Visitor): void {
visitor.visitCircle(this);
}
}
class Square implements Element {
constructor(readonly side: number) {}
accept(visitor: Visitor): void {
visitor.visitSquare(this);
}
}
class AreaVisitor implements Visitor {
total = 0;
visitCircle(circle: Circle): void {
this.total += Math.PI * circle.radius ** 2;
}
visitSquare(square: Square): void {
this.total += square.side ** 2;
}
}
class ExportVisitor implements Visitor {
readonly lines: string[] = [];
visitCircle(circle: Circle): void {
this.lines.push(`<circle r='${circle.radius}'/>`);
}
visitSquare(square: Square): void {
this.lines.push(`<rect width='${square.side}'/>`);
}
}
const shapes: Element[] = [new Circle(1), new Square(2), new Circle(0.5)];
const area = new AreaVisitor();
shapes.forEach((shape) => shape.accept(area));
console.log(`total area: ${area.total.toFixed(2)}`);
const exporter = new ExportVisitor();
shapes.forEach((shape) => shape.accept(exporter));
console.log(exporter.lines.join("\n"));total area: 7.93
<circle r='1'/>
<rect width='2'/>
<circle r='0.5'/>All three print a total area of 7.93 and three SVG tags. Java prints 1.0 and 2.0 where the others print 1 and 2, because its fields are double, and Java writes a whole-number double with a decimal point.
Java names every visit method visit and lets overloading tell them apart. Overloading is resolved when the code is compiled, from the declared type, so the visitor could not choose by itself between visit(Circle) and visit(Square) for an Element of unknown kind. Inside Circle.accept, though, this is declared as a Circle, so the compiler picks visit(Circle) there. The accept method exists to make that one choice in the one place that knows the class. Python and TypeScript have no overloading of this kind, so they name each method after its element.
← → move through sections and cards