Visitor
Rectangle and Square each have area(), perimeter() and now to_json().
Each shape has one method, accept(visitor), that calls the visitor method for its own type (visit_rectangle or visit_square).
The classes are stable but you keep adding operations
The picture from life
A tax inspector visits different kinds of shops: a grocery, a jeweller, a restaurant. The shops do not know tax rules. They just say "come in" and the inspector applies the rule for that kind of shop. Next year a new inspector (fire safety) visits the same shops with a different checklist, and no shop is rebuilt.
The problem
What goes wrong when you do not use it.
Rectangle and Square each have area(), perimeter() and now to_json(). Every new operation (draw, export, scale) means opening every shape class, and the shape classes fill up with unrelated code.
- The classes are stable but you keep adding operations
- Operations need different code for each class
- You want to keep unrelated operations out of the classes
The idea
How the pattern fixes it.
Each shape has one method, accept(visitor), that calls the visitor method for its own type (visit_rectangle or visit_square). Each operation is a visitor class holding the code for all shapes. New operation, new visitor, no shape edited.
- implements
- calls / uses
Who is who
- Shape (element)
- accept(visitor): calls the right visit_ method.
- Rectangle, Square
- Concrete elements with their data.
- AreaVisitor, PerimeterVisitor
- One class per operation with a method per shape.
Build it in steps
- 1Give each element an accept(visitor).
- 2Declare visit_ methods, one per element type.
- 3Write a visitor class per operation.
- 4Walk the elements and call accept().
Watch it run
Step through the calls. Tap an arrow to jump to it.
Hand the visitor to the rectangle.
accept(area)The code
Python and Java. Try writing the classes yourself from the diagram first.
class Rectangle:
def __init__(self, width, height):
self.width, self.height = width, height
def accept(self, visitor):
return visitor.visit_rectangle(self) # tells the visitor "I am a rectangle"
class Square:
def __init__(self, side):
self.side = side
def accept(self, visitor):
return visitor.visit_square(self)
class AreaVisitor:
def visit_rectangle(self, rectangle):
return rectangle.width * rectangle.height
def visit_square(self, square):
return square.side * square.side
class PerimeterVisitor: # a new operation, no shape edited
def visit_rectangle(self, rectangle):
return 2 * (rectangle.width + rectangle.height)
def visit_square(self, square):
return 4 * square.side
def demo():
shapes = [Rectangle(3, 4), Square(3)]
return [s.accept(AreaVisitor()) for s in shapes], [s.accept(PerimeterVisitor()) for s in shapes]What you gain
- New operations without touching the classes
- Related code for one operation sits together
What it costs
- Adding a new element class means editing every visitor
- Elements must expose enough data for visitors
You have already met it in
- Compilers walking a syntax tree (type checking, code generation)
- Java's FileVisitor for walking directories
- Python's ast.NodeVisitor
Often mixed up with
- IteratorIterator decides how to walk; Visitor decides what to do at each stop.
- InterpreterInterpreter keeps interpret() inside each node; Visitor moves operations out into separate classes.
Make it stick
Model Circle and Triangle, then write AreaVisitor and a JsonVisitor. Add a third operation without editing the shapes.