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
| Role | In this example | Responsibility |
|---|---|---|
| Visitor | #visit_tower, #visit_enemy, #visit_terrain | One method per element type. |
| Concrete Visitor | Description, Glyphs | One operation over the elements. It collects its result as it visits. |
| Element | Tower, Enemy, Terrain | Accepts a visitor and calls the visit method for its own type. |
| Object Structure | Cell | Holds the elements and offers each one to the visitor. |
Ruby implementation
# 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 ? "=" : ".")
endIn 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 + moneymakesInteger#+callmoney.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
acceptmethods.
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
Dataelements,case thing in Tower(kind:, level:) then ...handles every type in one method, with noacceptat 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
casestatements 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.