Overview
The Visitor pattern addresses a fundamental problem in object-oriented design: adding new operations to existing class hierarchies without modifying those classes. When a system contains a stable structure of objects but requires frequent addition of new operations, the Visitor pattern provides a solution that maintains the Single Responsibility Principle and Open/Closed Principle.
The pattern works by moving operations out of the classes they operate on and into separate visitor classes. Each element in the object structure accepts a visitor and delegates the operation to it. The visitor then performs the operation specific to that element type through double dispatch - a technique where the method executed depends on both the visitor type and the element type.
Consider an abstract syntax tree (AST) for a programming language. The tree structure remains stable - nodes represent expressions, statements, and declarations. However, the operations performed on this tree vary widely: type checking, code generation, optimization, pretty printing, and static analysis. Without the Visitor pattern, adding each new operation requires modifying every node class. With the Visitor pattern, each operation becomes a separate visitor, leaving the node classes unchanged.
# AST node structure remains stable
class NumberNode
attr_reader :value
def initialize(value)
@value = value
end
def accept(visitor)
visitor.visit_number(self)
end
end
# New operations added as visitors without modifying nodes
class EvaluatorVisitor
def visit_number(node)
node.value
end
end
class PrinterVisitor
def visit_number(node)
node.value.to_s
end
end
The pattern originated from the Gang of Four design patterns book and has become essential in compiler construction, document processing systems, and any domain where object structures remain stable while operations on them evolve.
Key Principles
The Visitor pattern operates on several interrelated principles that define its structure and behavior.
Double Dispatch: The pattern employs double dispatch to determine which method to execute. Single dispatch, used in typical object-oriented method calls, selects a method based on the receiver object's type. Double dispatch selects a method based on two types: the visitor type and the element type. When an element calls accept(visitor), the element passes itself to the visitor, allowing the visitor to call the appropriate method based on the element's specific type.
# First dispatch: element.accept(visitor) - based on element type
# Second dispatch: visitor.visit_element(self) - based on visitor type
class BinaryNode
def accept(visitor)
visitor.visit_binary(self) # Second dispatch
end
end
node.accept(visitor) # First dispatch
Separation of Concerns: Operations separate from the data structure they operate on. Each visitor encapsulates a single operation across all element types. This separation allows the element classes to focus on representing data and structure while visitors focus on algorithms and transformations.
Element Interface Stability: The element classes define a stable accept method that rarely changes. New operations do not require modifications to existing elements. The accept method serves as the extension point for all future operations.
Visitor Interface Extension: Adding a new element type to the structure requires updating all existing visitors to handle the new type. This represents the pattern's primary trade-off: easy addition of operations but difficult addition of new element types.
Type-Specific Behavior: Each visitor implements type-specific methods for each element type in the structure. The visitor knows the concrete type of each element it visits, allowing type-specific operations without type casting or type checking in client code.
class Visitor
# Separate method for each element type
def visit_number(node)
# Number-specific logic
end
def visit_binary(node)
# Binary node-specific logic
end
def visit_unary(node)
# Unary node-specific logic
end
end
Traversal Strategy Independence: The pattern separates the traversal strategy from the operation. Elements can implement different traversal orders (depth-first, breadth-first) in their accept methods, or traversal can be delegated to a separate structure. Visitors need not concern themselves with navigation logic.
State Accumulation: Visitors can accumulate state across multiple visits. Unlike operations implemented as methods on elements, visitors exist as objects that maintain instance variables throughout the traversal. This capability proves valuable for operations that aggregate information or maintain context across multiple nodes.
class VariableCollectorVisitor
def initialize
@variables = Set.new
end
attr_reader :variables
def visit_variable(node)
@variables.add(node.name)
end
end
Ruby Implementation
Ruby's dynamic nature and support for blocks provides multiple approaches to implementing the Visitor pattern. The classical approach uses explicit visitor classes with type-specific methods.
# Element hierarchy for an expression tree
class Expression
def accept(visitor)
raise NotImplementedError, "Subclasses must implement accept"
end
end
class Number < Expression
attr_reader :value
def initialize(value)
@value = value
end
def accept(visitor)
visitor.visit_number(self)
end
end
class Addition < Expression
attr_reader :left, :right
def initialize(left, right)
@left = left
@right = right
end
def accept(visitor)
visitor.visit_addition(self)
end
end
class Multiplication < Expression
attr_reader :left, :right
def initialize(left, right)
@left = left
@right = right
end
def accept(visitor)
visitor.visit_multiplication(self)
end
end
# Visitor for evaluation
class EvaluatorVisitor
def visit_number(node)
node.value
end
def visit_addition(node)
node.left.accept(self) + node.right.accept(self)
end
def visit_multiplication(node)
node.left.accept(self) * node.right.accept(self)
end
end
# Usage
expr = Addition.new(
Number.new(5),
Multiplication.new(Number.new(3), Number.new(4))
)
# 5 + (3 * 4)
evaluator = EvaluatorVisitor.new
result = expr.accept(evaluator)
# => 17
Ruby's respond_to? method enables dynamic method dispatch without explicit type checking:
class DynamicVisitor
def visit(node)
method_name = "visit_#{node.class.name.downcase}"
if respond_to?(method_name, true)
send(method_name, node)
else
visit_default(node)
end
end
private
def visit_default(node)
raise "No visit method for #{node.class.name}"
end
end
class PrinterVisitor < DynamicVisitor
def initialize
@output = []
end
attr_reader :output
private
def visit_number(node)
@output << node.value.to_s
end
def visit_addition(node)
@output << "("
node.left.accept(self)
@output << " + "
node.right.accept(self)
@output << ")"
end
def visit_multiplication(node)
@output << "("
node.left.accept(self)
@output << " * "
node.right.accept(self)
@output << ")"
end
end
printer = PrinterVisitor.new
expr.accept(printer)
printer.output.join
# => "(5 + (3 * 4))"
For simple visitors, Ruby blocks provide a lightweight alternative:
class Expression
def accept(visitor = nil, &block)
if block_given?
yield self
else
visitor.visit(self)
end
end
end
# Block-based traversal
expr.accept { |node| puts node.class.name }
State accumulation in Ruby visitors benefits from hash-based storage:
class StatisticsVisitor
def initialize
@counts = Hash.new(0)
@depth = 0
@max_depth = 0
end
attr_reader :counts, :max_depth
def visit_number(node)
@counts[:numbers] += 1
@max_depth = [@max_depth, @depth].max
end
def visit_addition(node)
@counts[:additions] += 1
@depth += 1
node.left.accept(self)
node.right.accept(self)
@depth -= 1
end
def visit_multiplication(node)
@counts[:multiplications] += 1
@depth += 1
node.left.accept(self)
node.right.accept(self)
@depth -= 1
end
end
stats = StatisticsVisitor.new
expr.accept(stats)
stats.counts
# => {:additions=>1, :multiplications=>1, :numbers=>3}
stats.max_depth
# => 2
Module mixins enable shared visitor functionality:
module Visitable
def accept(visitor)
method_name = "visit_#{self.class.name.downcase}"
visitor.send(method_name, self)
end
end
class Number
include Visitable
attr_reader :value
def initialize(value)
@value = value
end
end
class Addition
include Visitable
attr_reader :left, :right
def initialize(left, right)
@left = left
@right = right
end
end
Practical Examples
Document Processing System: A document consists of various elements - paragraphs, images, tables, and headings. Different operations transform the document: HTML export, PDF generation, word counting, and spell checking.
class DocumentElement
def accept(visitor)
raise NotImplementedError
end
end
class Paragraph < DocumentElement
attr_reader :text
def initialize(text)
@text = text
end
def accept(visitor)
visitor.visit_paragraph(self)
end
end
class Image < DocumentElement
attr_reader :url, :alt_text
def initialize(url, alt_text)
@url = url
@alt_text = alt_text
end
def accept(visitor)
visitor.visit_image(self)
end
end
class Table < DocumentElement
attr_reader :rows
def initialize(rows)
@rows = rows
end
def accept(visitor)
visitor.visit_table(self)
end
end
class HTMLExportVisitor
def initialize
@html = []
end
def result
@html.join("\n")
end
def visit_paragraph(element)
@html << "<p>#{escape_html(element.text)}</p>"
end
def visit_image(element)
@html << "<img src=\"#{escape_html(element.url)}\" alt=\"#{escape_html(element.alt_text)}\">"
end
def visit_table(element)
@html << "<table>"
element.rows.each do |row|
@html << " <tr>"
row.each do |cell|
@html << " <td>#{escape_html(cell)}</td>"
end
@html << " </tr>"
end
@html << "</table>"
end
private
def escape_html(text)
text.to_s.gsub('&', '&').gsub('<', '<').gsub('>', '>')
end
end
class WordCountVisitor
def initialize
@count = 0
end
attr_reader :count
def visit_paragraph(element)
@count += element.text.split.size
end
def visit_image(element)
@count += element.alt_text.split.size
end
def visit_table(element)
element.rows.each do |row|
row.each do |cell|
@count += cell.to_s.split.size
end
end
end
end
# Usage
document = [
Paragraph.new("The visitor pattern separates operations from data structures."),
Image.new("/diagram.png", "Class diagram showing visitor pattern"),
Table.new([
["Pattern", "Category"],
["Visitor", "Behavioral"]
])
]
html_visitor = HTMLExportVisitor.new
document.each { |element| element.accept(html_visitor) }
html_visitor.result
# => "<p>The visitor pattern separates operations from data structures.</p>
# <img src=\"/diagram.png\" alt=\"Class diagram showing visitor pattern\">
# <table>...</table>"
word_counter = WordCountVisitor.new
document.each { |element| element.accept(word_counter) }
word_counter.count
# => 16
File System Operations: Different operations traverse a file system structure: size calculation, permission checking, search, and backup preparation.
class FileSystemNode
attr_reader :name
def initialize(name)
@name = name
end
def accept(visitor)
raise NotImplementedError
end
end
class File < FileSystemNode
attr_reader :size, :permissions
def initialize(name, size, permissions)
super(name)
@size = size
@permissions = permissions
end
def accept(visitor)
visitor.visit_file(self)
end
end
class Directory < FileSystemNode
attr_reader :children, :permissions
def initialize(name, permissions)
super(name)
@children = []
@permissions = permissions
end
def add(child)
@children << child
end
def accept(visitor)
visitor.visit_directory(self)
end
end
class SizeCalculatorVisitor
def initialize
@total = 0
end
attr_reader :total
def visit_file(file)
@total += file.size
end
def visit_directory(directory)
directory.children.each { |child| child.accept(self) }
end
end
class PermissionCheckerVisitor
def initialize(required_permission)
@required_permission = required_permission
@violations = []
end
attr_reader :violations
def visit_file(file)
unless file.permissions.include?(@required_permission)
@violations << file.name
end
end
def visit_directory(directory)
unless directory.permissions.include?(@required_permission)
@violations << directory.name
end
directory.children.each { |child| child.accept(self) }
end
end
class SearchVisitor
def initialize(pattern)
@pattern = pattern
@matches = []
end
attr_reader :matches
def visit_file(file)
@matches << file.name if file.name.match?(@pattern)
end
def visit_directory(directory)
@matches << directory.name if directory.name.match?(@pattern)
directory.children.each { |child| child.accept(self) }
end
end
# Build file system structure
root = Directory.new("/", "rwx")
etc = Directory.new("etc", "r-x")
etc.add(File.new("config.yml", 1024, "r--"))
etc.add(File.new("secrets.txt", 512, "---"))
usr = Directory.new("usr", "rwx")
usr.add(File.new("app.rb", 2048, "rw-"))
root.add(etc)
root.add(usr)
# Calculate total size
size_calc = SizeCalculatorVisitor.new
root.accept(size_calc)
size_calc.total
# => 3584
# Check read permissions
perm_check = PermissionCheckerVisitor.new("r")
root.accept(perm_check)
perm_check.violations
# => ["secrets.txt"]
# Search for Ruby files
search = SearchVisitor.new(/\.rb$/)
root.accept(search)
search.matches
# => ["app.rb"]
Abstract Syntax Tree Optimization: Compiler optimizations operate on AST nodes. Different optimization passes transform the tree: constant folding, dead code elimination, and common subexpression elimination.
class ASTNode
def accept(visitor)
raise NotImplementedError
end
end
class Constant < ASTNode
attr_reader :value
def initialize(value)
@value = value
end
def accept(visitor)
visitor.visit_constant(self)
end
end
class BinaryOp < ASTNode
attr_reader :operator, :left, :right
def initialize(operator, left, right)
@operator = operator
@left = left
@right = right
end
def accept(visitor)
visitor.visit_binary_op(self)
end
end
class Variable < ASTNode
attr_reader :name
def initialize(name)
@name = name
end
def accept(visitor)
visitor.visit_variable(self)
end
end
class ConstantFoldingVisitor
def visit_constant(node)
node
end
def visit_variable(node)
node
end
def visit_binary_op(node)
left = node.left.accept(self)
right = node.right.accept(self)
# Both operands are constants - fold the operation
if left.is_a?(Constant) && right.is_a?(Constant)
result = case node.operator
when :add then left.value + right.value
when :multiply then left.value * right.value
when :subtract then left.value - right.value
when :divide then left.value / right.value
end
return Constant.new(result)
end
BinaryOp.new(node.operator, left, right)
end
end
# Original: (2 + 3) * x
tree = BinaryOp.new(
:multiply,
BinaryOp.new(:add, Constant.new(2), Constant.new(3)),
Variable.new("x")
)
folder = ConstantFoldingVisitor.new
optimized = tree.accept(folder)
# Result: 5 * x (constant addition folded)
Design Considerations
The Visitor pattern suits specific design scenarios where its trade-offs align with system requirements.
When Element Structure Stabilizes: Use the Visitor pattern when the object structure reaches stability but operations on that structure continue to evolve. Compiler ASTs, document models, and UI component hierarchies often exhibit this characteristic. Adding new operations becomes trivial - create a new visitor class. The pattern becomes problematic when element types change frequently because each change requires updating all visitors.
When Type-Safe Dispatch Matters: The pattern provides compile-time type safety (in statically typed languages) or clear runtime type handling (in Ruby). Each visitor method explicitly handles one element type. This explicitness prevents the accidental handling of types through generic interfaces or type casting. Compare with the alternative of adding a method to each element class, which disperses type-specific logic across the codebase.
# Visitor approach: type-specific logic centralized
class PrintVisitor
def visit_number(node)
# All number printing logic here
end
def visit_string(node)
# All string printing logic here
end
end
# Alternative: type-specific logic dispersed
class Number
def print
# Number printing here
end
end
class String
def print
# String printing here
end
end
When Operations Access Multiple Element Types: Some operations require cooperation between different element types or accumulate state across the entire structure. Visitors maintain state as instance variables, naturally supporting such operations. Statistical analysis, code generation, and validation often need this capability.
class ValidationVisitor
def initialize
@declared_variables = Set.new
@used_variables = Set.new
@errors = []
end
def visit_declaration(node)
if @declared_variables.include?(node.name)
@errors << "Variable #{node.name} already declared"
end
@declared_variables.add(node.name)
end
def visit_variable_use(node)
@used_variables.add(node.name)
end
def validate
undefined = @used_variables - @declared_variables
@errors += undefined.map { |name| "Variable #{name} used but not declared" }
@errors
end
end
Performance Trade-offs: The pattern introduces indirection through double dispatch. Each operation requires two method calls: element.accept(visitor) and visitor.visit_element(element). For performance-critical code with simple operations, this overhead may exceed the operation itself. The pattern works best for complex operations where dispatch overhead becomes negligible.
Extensibility Analysis: The pattern trades one axis of extensibility for another. Adding operations: simple. Adding element types: complex. Evaluate which axis changes more frequently in the specific domain. Database query optimizers add new optimization passes (operations) frequently but rarely add new query node types. This pattern fits perfectly. Conversely, plugin systems with evolving element types but stable operations should avoid this pattern.
Alternative Patterns: Several alternatives exist depending on requirements. The Strategy pattern encapsulates algorithms but operates on a single object rather than a structure. The Interpreter pattern builds operations into element classes, the opposite of Visitor's approach. The Chain of Responsibility pattern sequences operations but does not provide type-specific behavior. Consider the Iterator pattern for simple traversals that do not require type-specific operations.
Language Feature Interaction: Ruby's dynamic typing and metaprogramming reduce some benefits of the Visitor pattern compared to statically typed languages. Method dispatch based on class name (send("visit_#{class_name}")) provides similar functionality without the formal pattern structure. However, the pattern still provides clear separation of concerns and explicit operation definitions that improve maintainability.
Common Patterns
Several variations and extensions of the Visitor pattern address specific requirements.
Reflective Visitor: Uses reflection or metaprogramming to eliminate explicit visit methods for each element type. Ruby's dynamic method dispatch enables this variation naturally.
class ReflectiveVisitor
def visit(node)
method_name = "visit_#{node.class.name.downcase}"
if respond_to?(method_name, true)
send(method_name, node)
else
visit_default(node)
end
end
protected
def visit_default(node)
# Default behavior for unhandled types
end
end
class LoggingVisitor < ReflectiveVisitor
private
def visit_number(node)
puts "Number: #{node.value}"
end
def visit_addition(node)
puts "Addition"
node.left.accept(self)
node.right.accept(self)
end
end
Acyclic Visitor: Eliminates the element interface dependency on the visitor interface using Ruby modules or multiple inheritance emulation. This variation prevents the rigid coupling between elements and visitors.
module Visitable
def accept(visitor)
visitor_interface = "#{self.class.name}Visitor"
if visitor.class.ancestors.any? { |a| a.name == visitor_interface }
method = "visit_#{self.class.name.downcase}"
visitor.send(method, self)
end
end
end
module NumberVisitor
def visit_number(node)
raise NotImplementedError
end
end
module AdditionVisitor
def visit_addition(node)
raise NotImplementedError
end
end
class EvaluatorVisitor
include NumberVisitor
include AdditionVisitor
def visit_number(node)
node.value
end
def visit_addition(node)
node.left.accept(self) + node.right.accept(self)
end
end
Hierarchical Visitor: Handles element hierarchies where elements contain other elements. The visitor manages traversal automatically rather than requiring each visit method to explicitly traverse children.
class HierarchicalVisitor
def visit(node)
# Pre-order processing
process_node(node)
# Traverse children
if node.respond_to?(:children)
node.children.each { |child| visit(child) }
end
# Post-order processing
post_process_node(node)
end
protected
def process_node(node)
method = "visit_#{node.class.name.downcase}"
send(method, node) if respond_to?(method, true)
end
def post_process_node(node)
method = "leave_#{node.class.name.downcase}"
send(method, node) if respond_to?(method, true)
end
end
class DepthTracker < HierarchicalVisitor
def initialize
@depth = 0
@max_depth = 0
end
attr_reader :max_depth
private
def process_node(node)
@depth += 1
@max_depth = [@max_depth, @depth].max
end
def post_process_node(node)
@depth -= 1
end
end
Composite Visitor: Combines multiple visitors into a single visitor that delegates to multiple operations. This pattern enables executing several operations in one traversal.
class CompositeVisitor
def initialize(*visitors)
@visitors = visitors
end
def method_missing(method, *args)
if method.to_s.start_with?('visit_')
@visitors.each { |visitor| visitor.send(method, *args) }
else
super
end
end
def respond_to_missing?(method, include_private = false)
method.to_s.start_with?('visit_') || super
end
end
counter = NodeCounterVisitor.new
printer = PrinterVisitor.new
combined = CompositeVisitor.new(counter, printer)
tree.accept(combined)
# Both counting and printing occur in single traversal
Transforming Visitor: Returns transformed versions of elements rather than performing side effects. This pattern enables creating modified copies of structures.
class TransformingVisitor
def visit_number(node)
Number.new(node.value * 2)
end
def visit_addition(node)
left = node.left.accept(self)
right = node.right.accept(self)
Addition.new(left, right)
end
def visit_multiplication(node)
left = node.left.accept(self)
right = node.right.accept(self)
Multiplication.new(left, right)
end
end
# Original: 5 + (3 * 4)
original = Addition.new(
Number.new(5),
Multiplication.new(Number.new(3), Number.new(4))
)
doubler = TransformingVisitor.new
transformed = original.accept(doubler)
# Result: 10 + (6 * 8)
Filtered Visitor: Processes only specific element types, ignoring others. This pattern simplifies visitors that operate on element subsets.
class FilteredVisitor
def initialize(target_classes)
@target_classes = target_classes
end
def visit(node)
if @target_classes.any? { |klass| node.is_a?(klass) }
process(node)
end
if node.respond_to?(:children)
node.children.each { |child| visit(child) }
end
end
protected
def process(node)
raise NotImplementedError
end
end
class NumberCollector < FilteredVisitor
def initialize
super([Number])
@numbers = []
end
attr_reader :numbers
protected
def process(node)
@numbers << node.value
end
end
Common Pitfalls
Several mistakes commonly occur when implementing or using the Visitor pattern.
Forgetting to Traverse Children: Visitors that handle hierarchical structures must remember to traverse children. The accept method in container elements should accept the visitor, but the visitor itself must call accept on child elements.
# INCORRECT: Visitor does not traverse children
class BadCounterVisitor
def visit_directory(dir)
@count += 1
# Missing: dir.children.each { |child| child.accept(self) }
end
end
# CORRECT: Visitor traverses children
class GoodCounterVisitor
def visit_directory(dir)
@count += 1
dir.children.each { |child| child.accept(self) }
end
end
Circular References: Object structures with circular references cause infinite loops in visitors. Track visited nodes to prevent revisiting.
class SafeVisitor
def initialize
@visited = Set.new
end
def visit(node)
return if @visited.include?(node.object_id)
@visited.add(node.object_id)
# Process node
process(node)
# Traverse children
if node.respond_to?(:children)
node.children.each { |child| visit(child) }
end
end
end
Mismatched Visitor and Element Interfaces: When adding new element types, developers must update all visitors. Forgetting to add methods for new types causes runtime errors when visitors encounter unknown elements.
# New element type added
class Subtraction < Expression
def accept(visitor)
visitor.visit_subtraction(self)
end
end
# INCORRECT: Visitor missing visit_subtraction method
class IncompleteEvaluator
def visit_number(node)
node.value
end
def visit_addition(node)
node.left.accept(self) + node.right.accept(self)
end
# Missing visit_subtraction - will raise NoMethodError
end
# CORRECT: Visitor implements all required methods
class CompleteEvaluator
def visit_number(node)
node.value
end
def visit_addition(node)
node.left.accept(self) + node.right.accept(self)
end
def visit_subtraction(node)
node.left.accept(self) - node.right.accept(self)
end
end
Stateful Visitor Reuse: Visitors that accumulate state should not be reused without resetting. Reusing stateful visitors produces incorrect results.
counter = NodeCounterVisitor.new
tree1.accept(counter)
puts counter.count # => 10
# INCORRECT: Reusing visitor without reset
tree2.accept(counter)
puts counter.count # => 20 (includes count from tree1)
# CORRECT: Reset before reuse
counter = NodeCounterVisitor.new
tree2.accept(counter)
puts counter.count # => 8 (correct count for tree2)
Breaking Encapsulation: Visitors sometimes access element internals that should remain private. This access couples visitors to element implementation details.
# POOR: Visitor accesses private implementation
class Node
def initialize
@internal_cache = {}
end
attr_reader :internal_cache # Exposing private state
end
class BadVisitor
def visit_node(node)
# Accessing implementation details
node.internal_cache.clear
end
end
# BETTER: Element provides public interface
class Node
def clear_cache
@internal_cache.clear
end
end
class GoodVisitor
def visit_node(node)
node.clear_cache
end
end
Incorrect Double Dispatch: Implementing accept incorrectly breaks the double dispatch mechanism. The element must pass itself to the visitor method.
# INCORRECT: Not passing self
class BadElement
def accept(visitor)
visitor.visit_element # Wrong: no argument
end
end
# CORRECT: Passing self enables double dispatch
class GoodElement
def accept(visitor)
visitor.visit_element(self) # Correct: passes self
end
end
Type Checking in Visitors: Adding type checks in visitor methods defeats the pattern's purpose. Type-specific behavior should use separate visit methods, not conditionals.
# POOR: Type checking in visitor
class BadVisitor
def visit_node(node)
if node.is_a?(Number)
# Handle number
elsif node.is_a?(Addition)
# Handle addition
end
end
end
# BETTER: Separate methods for each type
class GoodVisitor
def visit_number(node)
# Handle number
end
def visit_addition(node)
# Handle addition
end
end
Reference
Core Components
| Component | Responsibility | Implementation |
|---|---|---|
| Visitor | Defines visit methods for each element type | Abstract class or module with visit methods |
| ConcreteVisitor | Implements operations for each element type | Class implementing all visit methods |
| Element | Defines accept method | Abstract class or module with accept |
| ConcreteElement | Implements accept to call appropriate visitor method | Class with accept calling visitor |
| ObjectStructure | Contains elements and provides traversal | Collection or composite structure |
Method Signatures
| Method | Parameter | Return Type | Purpose |
|---|---|---|---|
| accept | visitor | Varies | Enable double dispatch |
| visit_type | element | Varies | Process specific element type |
Implementation Checklist
| Step | Description | Verification |
|---|---|---|
| 1 | Define element interface with accept method | All elements implement accept |
| 2 | Create visitor interface with visit methods for each element type | Visit method exists for each element |
| 3 | Implement accept in each concrete element | Accept calls correct visitor method |
| 4 | Implement concrete visitors with type-specific logic | All visit methods implemented |
| 5 | Manage state in visitor instance variables if needed | State reset between uses |
| 6 | Handle traversal of composite structures | Children visited recursively |
| 7 | Consider circular references | Visited nodes tracked |
Common Visitor Types
| Visitor Type | Purpose | Return Value | State |
|---|---|---|---|
| Transformer | Create modified structure | New element | None |
| Evaluator | Compute values | Computed result | Accumulator |
| Collector | Gather information | Collection | Collection |
| Validator | Check constraints | Errors/warnings | Error list |
| Printer | Generate output | String/IO | Output buffer |
| Counter | Count elements | Numeric count | Counter |
Decision Matrix
| Requirement | Use Visitor | Alternative |
|---|---|---|
| Stable element types, evolving operations | Yes | Strategy for single operations |
| Frequently changing element types | No | Operations in elements |
| Type-specific behavior needed | Yes | Dynamic dispatch with conditionals |
| Operations need state across traversal | Yes | Fold/reduce with accumulator |
| Simple single-pass transformation | No | Map/transform methods |
| Multiple operations in one pass | Yes | Fold with multiple accumulators |
Ruby Idioms
| Pattern | Ruby Approach | Example |
|---|---|---|
| Dynamic dispatch | Use send with computed method name | send("visit_" + node.class.name.downcase, node) |
| Method existence check | Use respond_to? | respond_to?("visit_number", true) |
| Default behavior | Use method_missing | def method_missing(method, *args) |
| Shared functionality | Use modules | module Visitable |
| State initialization | Use initialize with instance variables | def initialize; @count = 0; end |
| Block-based visiting | Accept block parameter | def accept(visitor = nil, &block) |