login

VisitorPattern (Ruby)

HomePage | RecentChanges | Preferences | Wikis | RubyGarden | Feed-icon-16x16

"Visitor lets you define a new operation without changing the classes of the elements on which it operates." [1]

The primary purpose of the Visitor pattern is to double dispatch on both the class of object implementing an operation and the classes of the elements being operated upon. Since Ruby implements single-dispatch polymorphism, the each method does not do enough to implement the Visitor pattern.

Canonical Visitor Example

Here's how the visitor pattern would normally be implemented in a single-dispatch language. However, Ruby has another, easier way to achieve the same end (see below).

To start, lets define a simple class hierarchy defining shapes (rectangle, line) and graphical composition (group)

class Shape
end

class Rectangle < Shape
    attr_accessor :x, :y, :width, :height
    
    def initialize( x, y, width, height )
        @x = x; @y = y; @width = width; @height = height
    end
    
    def union( rect )
        ... # returns the union of two rectangles
    end
end

class Line < Shape
    attr_accessor :x1, :y1, :x2, :y2
    
    def initialize( x1, y1, x2, y2 )
        @x1 = x1; @y1 = y1; @x2 = x2; @y2 = y2
    end
end

class Group < Shape
    add( shape ) ...    # adds a shape to the group
    remove( shape ) ... # removes a shape from the group
    each ...            # iterates over shapes in the group
end

The visitor pattern defines different operations on shapes and groups as classes with methods that implement the operation for each type of shape. This involves adding an 'accept' method in each shape class that takes a visitor as an argument and calls back to the visitor describing the type of shape. (This is the double-dispatch).

class Line
    def accept( visitor )
        visitor.visit_line( self )
    end
end

class Rectangle
    def accept( visitor )
        visitor.visit_rectangle( self )
    end
end
 
class Group
    def accept( visitor )
        visitor.visit_group( self )
    end
end

Visitor classes can now be defined to implement different operations -- calculate the total area of the shapes, find the bounding rectangle, draw the shapes, turn the shapes into outlines, etc. For example:

class BoundingRectangleVisitor
    attr_reader :bounds
    
    def initialize
        @bounds = nil
    end
    
    def visit_rectangle( rectangle )
        if @bounds
            @bounds = @bounds.union( rectangle )
        else
            @bounds = rectangle
        end
    end
    
    def visit_line( line )
        line_bounds = Rectangle.new( x1, y1, x2-y1, x2-y2 )
        if @bounds
            @bounds = @bounds.union( line_bounds )
        else
            @bounds = line_bounds
        end
    end
    
    # Visit each of the members of the group.
    def visit_group( group )
        group.each { |shape| shape.accept( self ) }
    end
end

The visitor would be used in this way:

shape = get_shape_from_somewhere
visitor = BoundingRectangleVisitor.new
shape.accept( visitor )
bounding_box = visitor.bounds

The Ruby Way

However, Ruby has another mechanism that can make the Visitor pattern unnecessary -- open class definitions. In Ruby it is possible to add new features to a class without changing the original definition. Just define new methods within a class statement for an existing class name. The eagle-eyed reader will have noticed that we already used this mechanism to add the accept methods for the visitor pattern to the existing shape classes.

For example, to add bounding rectangle calculations to the shapes:

require 'shapes' # assuming we saved the shape classes in shapes.rb

class Rectangle
    def bounds
        Rectangle.new( x, y, width, height )
    end
end

class Line
    def bounds
        Rectangle.new( x1, y1, x2-y1, x2-y2 )
    end
end
 
class Group
    def bounds
        rect = nil
        
        each do |s|
            if rect
                rect = rect.union( s.bounds )
            else
                rect = s.bounds
            end 
        end
        rect
    end
end

I think you'll agree, the Ruby way is much simpler.

-- NatPryce

The Ruby Metaprogramming Way

While it is certainly true the above example will provide an addequate solution in many situations it will have some issues in a large type hiearchy like a set of nodes generated for a compiler. There are a few problems with the above system:

The following method using Visitable module allows for all of these things, and still seems reasonably rubyish. It exploits the ease with which one can walk the inheritance tree and define new methods based on events.

#file -- visitor.rb

module Visitable
  def accept(visitor,&block)
    for klass in self.class.ancestors do
      break if (v = visitor.methods.include?("visit_#{klass.name.split(/::/)[-1]}"))
    end
    if v
      visitor.__send__("visit_#{klass.name.split(/::/)[-1]}",self,&block)
    else
      visitor.default_visit(self,&block)
    end
  end
  def Visitable.included(kls)
    kls.module_eval <<-"end_eval"
    def self.inherited(child)
      child.module_eval "include Visitable"
    end
    end_eval
  end
end

To use, just require the library, and include Visitable in the root nodes of your class hiearchy, and then define some visitors classes which have a visit_#{Klassname} method for each class you wish to process. If you want a default fall back visitor just define a default_visit method. To start it just call something like:

  root.accept(Visitor.new)  

Below is the example test code and output for the Visitable module. Note how you can selectively include and exclude parts of the inheritance tree. It will also match based on any mixins at the point it was added to the tree.

require 'visitor'

module Children
  def initialize(*children)
    @children = children
  end
  def children
    @children
  end
end

class Root
  include Visitable
  include Children
end
class Root2
  include Visitable
  include Children
end
class Root3
  include Visitable
  include Children
end

module Test
end

class RootChild1 < Root
end
class Root2Child1 < Root2
end
class Root2Child2 < Root2Child1
  include Test
end
class Root2Child3 < Root2Child2
end
class Root3Child1 < Root3
end
class Root3Child2 < Root3Child1
end

class Visitor
  def initialize()
    @name = self.class
  end
  def visitChildren(klass)
    klass.children.each {|child| child.accept(self)}
  end
  def default_visit(o)
    puts "#{@name} - default_visit: #{o.class}"
    visitChildren(o)
  end
  def visit_Root(o)
    puts "#{@name} - visit_Root: #{o.class}"
    visitChildren(o)
  end
  def visit_Root2(o)
    puts "#{@name} - visit_Root2: #{o.class}"
    visitChildren(o)
  end
  def visit_Test(o)
    puts "#{@name} - visit_Test: #{o.class}"
    visitChildren(o)
  end
  def visit_Root3Child1(o)
    puts "#{@name} - visit_Root3Child1: #{o.class}"
    visitChildren(o)
  end
end

class Visitor2 < Visitor
  def initialize(name)
    @name = "Visitor2<#{name}>"
  end
  def visit_Root(o)
    puts "#{@name} - visit_Root: #{o.class}"
    o.children.each {|child| child.accept(Visitor2.new(child.class))}
  end
  def visit_Root2(o)
    puts "#{@name} - visit_Root2: #{o.class}"
    o.children.first.accept(self)
    o.children.last.accept(Visitor.new)
  end
end

class Visitor3
  def default_visit(o,&block)
    yield o if block_given?
    o.children.each {|child| child.accept(self,&block)}
  end
end

root = Root.new(
                Root2.new(
                          Root3.new(RootChild1.new,
                                    Root3Child1.new),
                          Root3Child2.new),
                Root2Child2.new(
                                Root2Child3.new))
puts "# Visitor -- Default Visitor Behavior"
root.accept(Visitor.new)
puts "# Visitor 2 -- Derived Visitor Behavior"
root.accept(Visitor2.new("ROOT"))
puts "# Visitor 3 -- Passing A Block"
root.accept(Visitor3.new) {|root| puts root.class}

Which produces several renditions of a depth first traversal of the graph:

# Visitor -- Default Visitor Behavior
Visitor - visit_Root: Root                 # found exact type match
Visitor - visit_Root2: Root2               # same
Visitor - default_visit: Root3              # no match, called defaultVisit
Visitor - visit_Root: RootChild1           # no match, found match on parent
Visitor - visit_Root3Child1: Root3Child1   # exact match, despite parent having no match
Visitor - visit_Root3Child1: Root3Child2   # no match, found match on parent       
Visitor - visit_Test: Root2Child2          # no match, found match on mixin
Visitor - visit_Test: Root2Child3          # no match, found match on mixin of parent
# Visitor 2 -- Derived Visitor Behavior
Visitor2<ROOT> - visit_Root: Root                 
Visitor2<Root2> - visit_Root2: Root2               
Visitor2<Root2> - default_visit: Root3
Visitor2<Root2> - visit_Root: RootChild1
Visitor2<Root2> - visit_Root3Child1: Root3Child1
Visitor - visit_Root3Child1: Root3Child2
Visitor2<Root2Child2> - visit_Test: Root2Child2
Visitor2<Root2Child2> - visit_Test: Root2Child3
# Visitor 3 -- Passing A Block
Root
Root2
Root3
RootChild1
Root3Child1
Root3Child2
Root2Child2

The second example of the output shows how you can use different visitors to walk other parts of the tree with behavioral differences. This is very useful for applications such as compilers, where you may find it useful to say walk the parameter list of a function in order to generate the type declaration. Also note the use of inheritence of visitors to specify different behavior.

The third example shows the ability to pass down a block through the visitor, allowing you to use a proc to apply a method across the hiearchy. This allows you to make template methods out of some of your visitors.

A neat trick that dovetails with this depends on how you define your initializer for your Visitor. If you define your initializer like this:

  def initialize(target = nil,&block)
    target.accept(self,&block) if target
  end

Then you can visit a hiearchy using

  Visitor3.new(root) {|x| ... }

Instead of

  root.accept(Visitor3.new) {|x| ... }

All depends on your preference but I find the first method slightly more readable.

Note: there is a bug in that if you use a class named A::B and a class named B there will be a name collision, ie both classes will match. I'm trying to find an alternate solution to this problem.

-- CharlesComstock


Note: there is a typo in the pickaxe book. Where they mention the VisitorPattern [2], the authors really mean the IteratorPattern.


I've built an abstract visitor class in Smalltalk using code generation.

Lets say you've got a class hierarchy with classes Root, Sub and SubSub. I made root and it's subclasses visitable by sending visitClassname to the parameter in method visit. Then I generated an AbstractVisitor with method visitRoot as subclass responsibility (=abstract method), method visitSub sending visitRoot by default and visitSubSub sending visitSub.

Whenever I sublass this AbstractVisitor the only thing to get this NewVisitor work in the first place is to implement NewVisitor>>visitRoot. Whenever I change the class hierarchy below Root, I just regenerate the AbstractVisitor and most of my code will still work, no matter how many visitor subclasses I've allready built.

-- SaschaDoerdelmann


Also see: ExampleDesignPatternsInRuby IterationStyles

HomePage | RecentChanges | Preferences | Wikis | RubyGarden
Edit text of this page | View other revisions
Rev 35, Last edited at March 24, 2006 14:36 pm by rgNoNameGiven / hq.tensilica.com (diff)
Find: