Visitor

Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

Participants

RoleIn this exampleResponsibility
Visitor#visit_tower, #visit_enemy, #visit_terrainOne method per element type.
Concrete VisitorDescription, GlyphsOne operation over the elements. It collects its result as it visits.
ElementTower, Enemy, TerrainAccepts a visitor and calls the visit method for its own type.
Object StructureCellHolds the elements and offers each one to the visitor.

Ruby implementation

visitor.rb
# Elements: each one accepts a visitor and calls back the method meant for
# its own type. That second call is the "double dispatch".
Tower = Data.define(:kind, :level) do
  def accept(visitor) = visitor.visit_tower(self)
end

Enemy = Data.define(:kind, :hp) do
  def accept(visitor) = visitor.visit_enemy(self)
end

Terrain = Data.define(:kind) do
  def accept(visitor) = visitor.visit_terrain(self)
end

# The Object Structure: everything under the cursor, or on the whole map.
class Cell
  def initialize(*things)
    @things = things
  end

  def accept(visitor)
    @things.each { |thing| thing.accept(visitor) }
    visitor
  end
end

# Concrete Visitors: new operations over the elements, added without
# touching the element classes.
class Description
  attr_reader :lines

  def initialize
    @lines = []
  end

  def visit_tower(tower) = @lines << "#{tower.kind.capitalize} tower, level #{tower.level}"
  def visit_enemy(enemy) = @lines << "#{enemy.kind.capitalize} with #{enemy.hp} hp"
  def visit_terrain(terrain) = @lines << (terrain.kind == :path ? "Path: enemies walk here" : "Grass: you can build here")
end

class Glyphs
  attr_reader :text

  def initialize
    @text = +""
  end

  def visit_tower(tower) = @text << tower.kind[0].upcase
  def visit_enemy(enemy) = @text << enemy.kind[0]
  def visit_terrain(terrain) = @text << (terrain.kind == :path ? "=" : ".")
end

In use

cell = Cell.new(Terrain.new(:path), Enemy.new(:goblin, 18))

cell.accept(Description.new).lines
# => ["Path: enemies walk here", "Goblin with 18 hp"]

Cell.new(Terrain.new(:grass), Tower.new(:arrow, 2)).accept(Glyphs.new).text
# => ".A"

Need a threat report too? Write ThreatReport with three visit_ methods. Tower, Enemy and Terrain don’t change, and there’s no case thing.kind anywhere.

Visitor and double dispatch

They’re related, but they’re not the same thing.

Double dispatch is a technique: choosing which code runs based on the runtime types of two objects. Ruby only dispatches on the receiver, so thing.accept(visitor) picks code from thing’s class alone. Calling back, with visitor.visit_enemy(self), makes a second choice based on the visitor’s class. Together the two calls select the right code for that element and that visitor.

Visitor is a pattern with a goal: adding new operations over a fixed set of element classes without changing them. Double dispatch is how it does that in languages like Ruby, Java and C#. But the pattern is also the object structure to walk, one class per operation, and a deliberate trade-off (see below).

The two turn up apart, too:

  • Double dispatch without Visitor: Ruby’s numeric coerce. 1 + money makes Integer#+ call money.coerce(1), so the right class handles the sum. No structure is being walked; two types just meet.
  • Visitor without hand-written double dispatch: languages with multiple dispatch, such as Julia, Common Lisp or Clojure’s multimethods, choose a method from every argument’s type for you. Operations can still be organised as visitors, with no accept methods.

Notes

Ruby idioms

  • Each element names its own visit method: visitor.visit_tower(self). Ruby has no method overloading, so the method name carries the type.
  • Pattern matching is a lighter alternative. With Data elements, case thing in Tower(kind:, level:) then ... handles every type in one method, with no accept at all.
  • Parsers and linters like RuboCop and Prism walk syntax trees with visitors (on_send, visit_call_node).

Reach for it when

  • The set of element classes is stable, but you keep adding operations over them: descriptions, map glyphs, reports, exports.
  • You want each operation’s logic kept together in one class, instead of case statements spread across the UI.

Watch for

  • Adding a new element type means updating every visitor. Visitor makes new operations cheap and new element types expensive, which is the reverse of ordinary polymorphism.

Visitor makes new operations cheap and new element types expensive.