GotchaDifficulty: Advanced2 min

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.
py
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)
OutputRan with python:3.12-slim (Python 3.12.13)
TypeError: Can't instantiate abstract class AreaVisitor without an implementation for abstract method 'visit_triangle'
The interface has three abstract methods, and `AreaVisitor` defines two.

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.

Classes touched by each kind of change, with three element classes and four operations
ChangeA method per operation on each classVisitor
A new operationAll 3 element classes1 new visitor class
A new element class1 new element class1 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.