CrackedRuby CrackedRuby

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('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;')
  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)