skip to content

What is the Visitor pattern's central trade-off regarding adding new element types versus new operations?

level: seniorimportance: should knowfreq 50%

answer

  1. 2-D grid: rows=element types, columns=operations
  2. Visitor: cheap columns, expensive rows
  3. Polymorphism: cheap rows, expensive columns
  4. Expression problem (Wadler)
  5. Pick Visitor when elements are stable

basics

~20 s

Visitor makes adding new operations easy (write one new visitor, touch no element classes) but makes adding new element types hard (you must add a visit method to every existing visitor). Plain polymorphism is the reverse.

solid answer

~50 s

Every design has two axes of change: adding new *element types* (data variants) and adding new *operations* (behaviours). You can usually make only one cheap. Plain object-oriented polymorphism (a method per behaviour on each class) makes adding a new *element type* cheap — write one class implementing all methods — but adding a new *operation* expensive, since you edit every class. Visitor inverts this: a new *operation* is one new visitor class with no edits to elements, but a new *element type* forces a new method on the Visitor interface and therefore an edit to *every* existing visitor (and recompilation). This tension is the **expression problem**: no classic technique makes both axes cheap and type-safe simultaneously. So you choose Visitor precisely when element types are stable and operations grow, and you choose plain polymorphism when the opposite holds.

go deeper

for a junior

Can state that Visitor makes new operations easy but new element types hard.

for a middle

Explains the mirror-image relationship with plain polymorphism using the operation-vs-element framing.

for a senior

Names the expression problem, reasons about which axis a given domain favours, and notes secondary costs like scattered behaviour and weakened encapsulation.

for a principal

Discusses the four constraints of the expression problem and how language features (sealed types, pattern matching, type classes, multimethods) move the trade-off frontier.

## Two axes of change Think of a 2-D grid. Rows are **element types** (`NumberNode`, `AddNode`, `MulNode`). Columns are **operations** (`evaluate`, `print`, `typeCheck`). Each cell is "what does this operation do for this element". A growing system adds rows (new data variants) and/or columns (new behaviours). The design question: *which direction of growth is cheap?* ## Plain polymorphism: rows are cheap, columns are expensive If each operation is a method on each element class, then the code is grouped **by row** (all of `AddNode`'s behaviours live in `AddNode`). - **Add a new element type (row):** write one new class implementing the existing operation methods. Cheap, localized, no edits elsewhere. ✅ - **Add a new operation (column):** add a method to the base type and implement it in *every* element class. Expensive, touches every row. ❌ ## Visitor: columns are cheap, rows are expensive Visitor groups code **by column** (all of `evaluate`'s logic lives in `EvaluateVisitor`). - **Add a new operation (column):** write one new `Visitor` implementation. No element class changes. Cheap. ✅ - **Add a new element type (row):** add `visit(NewNode)` to the `Visitor` interface, which breaks/forces an edit in *every* existing visitor implementation, plus an `accept` on the new element. Expensive, touches every column. ❌ Notice the symmetry: Visitor and plain polymorphism are mirror images. They optimise opposite axes. ## The expression problem This fundamental tension has a name — the **expression problem** (coined by Philip Wadler): can you add both new data variants *and* new operations to a datatype, without modifying existing code, *and* keep static type safety, *and* avoid recompiling old code? Classic OO picks easy-rows; classic functional/Visitor picks easy-columns; neither single technique wins both in a plain language. (Type classes, multimethods, and some modern features get closer.) ## How to decide in practice Ask: *which axis changes more often in this domain?* - **Stable element set, growing operations** → Visitor. Compilers/interpreters are the canonical fit: the language grammar (node types) changes rarely, but you keep adding passes (optimise, lint, generate code). - **Growing element set, stable operations** → plain polymorphism. E.g. a UI widget framework where third parties add widgets but the core operations (render, measure) are fixed. - **Both grow, and you control the language** → consider sealed types + pattern-matching switch (covered separately), which makes adding operations cheap *and* gives compiler-checked exhaustiveness when you add element types, mitigating Visitor's main pain. ## A subtle cost beyond the axes Visitor also disperses one element's behaviour across many visitor files (you can't read everything `AddNode` does in one place), and it can require exposing element internals to visitors (weakening encapsulation). Weigh these against the operation-extensibility win. ## One-line summary Visitor buys cheap new operations by selling expensive new element types — pick it only when the element hierarchy is stable.

  • What is the expression problem?
    The challenge of extending a datatype in both directions — new variants and new operations — without modifying existing code, while keeping static type safety and avoiding recompilation. No classic single technique solves all four constraints at once.
  • Beyond the add-axis cost, what other downside does Visitor have?
    It scatters one element type's behaviour across many visitor classes (harder to read everything that element does in one place) and often forces elements to expose internal state to visitors, weakening encapsulation.

saying these in an interview costs you the question

  • Saying Visitor makes both axes cheap
  • Recommending Visitor for a rapidly-growing element hierarchy
  • Not recognizing the mirror-image relationship with plain polymorphism
  • Ignoring the recompile/edit-every-visitor cost of a new element type

context