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
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.
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.