Card 07/ 09
Adding an Element Class Breaks Every Visitor
A new shape needs a new visit method in every visitor that exists.
Visitor makes a new operation cheap and a new element class expensive, because every visitor must gain a method for the new class. Choosing Visitor is a bet that operations will change more often than element classes. This card shows what happens when a class is added, and how to judge the bet.
The shapes gain a Triangle, and the visitor interface gains an abstract visit_triangle. AreaVisitor, written before triangles existed, is unchanged.
What happens when the program creates an AreaVisitor? Predict, then look.
from __future__ import annotations
from abc import ABC, abstractmethod
class Visitor(ABC):
@abstractmethod
def visit_circle(self, circle: Circle) -> None: ...
@abstractmethod
def visit_square(self, square: Square) -> None: ...
@abstractmethod
def visit_triangle(self, triangle: Triangle) -> None: ...
class Circle:
def accept(self, visitor: Visitor) -> None:
visitor.visit_circle(self)
class Square:
def accept(self, visitor: Visitor) -> None:
visitor.visit_square(self)
class Triangle:
def accept(self, visitor: Visitor) -> None:
visitor.visit_triangle(self)
class AreaVisitor(Visitor):
def visit_circle(self, circle: Circle) -> None:
print("area of a circle")
def visit_square(self, square: Square) -> None:
print("area of a square")
try:
AreaVisitor()
except TypeError as error:
print("TypeError:", error)TypeError: Can't instantiate abstract class AreaVisitor without an implementation for abstract method 'visit_triangle'Python refuses to create the visitor at all. Because the visit methods are abstract, every visitor without visit_triangle fails as soon as it is created, which finds them all on the first run. Without @abstractmethod, the failure would come later, as an AttributeError the first time a triangle met an old visitor.
The error is the useful outcome. Every visitor in the codebase must now be edited, and the abstract method lists them for you. With ten visitors, one new shape is ten edits; with a method per operation on each class, it would have been one new class.
| Change | A method per operation on each class | Visitor |
|---|---|---|
| A new operation | All 3 element classes | 1 new visitor class |
| A new element class | 1 new element class | 1 new element class, and all 4 visitors |
The two designs are mirror images, and neither is cheaper overall. Visitor fits when the element classes are fixed by something outside the code, such as the grammar of a language or a file format, and the operations keep growing. When new element classes arrive as often as new operations, methods on the classes are the better choice.
← → move through sections and cards