Visitor
Occasional. Turns up in specific problems. Worth recognising and being able to sketch, not worth drilling.
Start with the problem
corpus/snake-and-ladder's SquareEffect is a closed set: a square is plain, a snake's head, or a ladder's foot, and nothing else.
public sealed interface SquareEffect permits Plain, Snake, Ladder {}
Today there is exactly one operation on this type: apply the square's effect when a player lands on it. A switch inside the game loop handles all three cases, and that is fine, because there is one operation to write.
Watch where it goes
A board-rendering feature needs a description string for every square. "Nothing here." "Slide down to 14." "Climb up to 52." That is a second operation over the same three types. A debug tool then asks how many of each kind sit on the board, a third operation. Each new operation over Plain, Snake and Ladder is fine on its own. But suppose these types themselves had to grow a method for every operation instead, applyEffect(), describe(), countAs(). The three data classes would then carry every unrelated feature that ever needed to ask a question about them.
The move, GoF's version
The classic answer is Visitor. Give the hierarchy an accept method that calls back into a visitor object, one method per subtype. A new operation is then a new visitor class, not a new method on Plain, Snake and Ladder.
interface SquareEffectVisitor<R> {
R visitPlain(Plain p);
R visitSnake(Snake s);
R visitLadder(Ladder l);
}
Each type gets an accept method that calls the matching visit method on whatever visitor it is handed. A new operation, say the description feature, is then one new class implementing SquareEffectVisitor<String>, with no edit to Plain, Snake or Ladder.
What modern Java changes here
Java 21 gives this exact problem a smaller answer: since SquareEffect is already sealed, a switch over it is exhaustive-checked by the compiler, with no default branch needed.
static String describe(SquareEffect effect) {
return switch (effect) {
case Plain p -> "nothing here";
case Snake s -> "slide down to " + s.tail();
case Ladder l -> "climb up to " + l.top();
};
}
If a fourth kind of square is ever added to the permits list, every switch written this way stops compiling. A new case is required, named by file and line before any test runs.
That is the same guarantee Visitor's double dispatch was built to provide, at the cost of an accept method on every type and a visitor interface with one method per type. Sealed types plus pattern-matching switch, finalized as JEP 441, buy the same exhaustiveness with neither. Record patterns go one step further. A case for Ladder can destructure its fields directly, case Ladder(int foot, int top) -> ..., without calling an accessor inside a hand-written visitLadder.
When naming it is wrong
Sealed types and switch only work when the type list lives in this codebase. Visitor is still the right answer when the hierarchy is not closed by you. A plugin system is the usual case. Another team supplies its own node types, and you cannot add an accept method to a class you do not own, or seal a permits list around kinds you cannot enumerate. corpus/snake-and-ladder's own contract states the test for this directly: "the next kind gets written by ______."
If the blank names another team or a plugin, neither sealed nor switch can close the set. That is the one place Visitor still earns its keep in Java, because accept lets a type you never compiled against cooperate with an operation it does not know exists.
Where this lives in the app
The anchor given for this page is A3, and it is a strong one, verified. corpus/snake-and-ladder's SquareEffect is exactly the closed hierarchy described above, and A3's own lesson measures the cost of the sealed-and-switch discipline on this type directly. No file anywhere in this corpus is named Visitor, and no accept/visit double dispatch exists in any reference solution. That absence fits the story above. Every closed hierarchy in this app is owned locally, so sealed plus switch covers it. Visitor's actual justification, a hierarchy owned by someone else, never comes up in these problems.