General

Gems

Web Based

Visitor Pattern in Ruby

A look at the visitor pattern, 7 Sep 2011

So it's hard to talk about patterns in Ruby, but this one I think will be fun :), I'll try to show you a basic representation of the original idea, compare that to something you know, then expand it to the full idea and show you how I've interpreted it.

Basic example

So, if you can read UML, then there's some UML diagrams that explain what this looks like. I had to study them a while and have my mentor Doug explain them to me several times. But the gist of the idea is that you have a set of objects you want to access (for no specific definition of "set", it honestly doesn't even need to be a collection, though that's all I show here). But in statically typed languages, you can't just iterate over them, because that would require you to go to the set and write a method for each of the objects you passed through, a violation of the Open/Closed Principle. So instead, you create two interfaces. The first interface is the Visitor, and the second is the Visited.

  • The Visitor interface says that this object has a method "visit", whose param is a Visited.
  • The Visited interface says that this object has a method "accept" whose param is a Visitor.

The visited accepts the visitor, and then passes itself to the visitor's visit method. Here's the simplest code example I could think of to illustrate this.

class Visitor
  def visit(visited)
    @today_i_saw ||= []
    @today_i_saw << visited.name
  end
  
  def seen_today
    "Today I saw #{@today_i_saw.join ', '}"
  end
end

class Visited < Struct.new(:name, :friend)
  def accept(visitor)
    visitor.visit(self)
    friend.accept visitor if friend
  end
end

visiteds = Visited.new("Carl", Visited.new("Sal", Visited.new("Maude")))
visitor = Visitor.new

visiteds.accept visitor

visitor.seen_today # => "Today I saw Carl, Sal, Maude"

Okay, so so why the song and dance? Well, who knows about iterating over the visiteds? The visiteds do, so they need a reference to the visitor. And who knows what to do with the visiteds? The visitor does, so it needs a link to each visited. Thus we pass to an object which passes itself to the object we gave it.

Compared to iterating with each

Now, if you're like me, you're probably thinking "why wouldn't you just pass a block and iterate?", so let's take a look at that.

class Visited < Struct.new(:name, :friend)
  def each(&block)
    block.call(self)
    friend.each(&block) if friend
  end
end

visiteds = Visited.new("Carl", Visited.new("Sal", Visited.new("Maude")))

today_i_saw = []
visiteds.each { |visited| today_i_saw << visited.name }

"Today I saw #{today_i_saw.join ', '}" # => "Today I saw Carl, Sal, Maude"

Notice the similarity between the Visited#each above and Visited#accept from the previous example, it's basically the same method, but since we aren't constrained by types (interfaces are types), we can take advantage of the block. The block is actually the visitor, any object which implements #each in this manner can be visited.

Okay, so that's the basic part of it, I think of it that way because I first got interested in it when Corey Haines tweeted 1 2 about it as a solution to traversing a tree.

The full pattern

Now, in the examples, there are lots of visiteds, which all implement the Visited interface, so the visitor overloads its visit method for each of these subtypes. In Java you can do this, because it's only interested in method signatures. In Ruby, it's about messages being passed around, each message is handled by one function (there are actually some nuances here, but not really the point of this post). So in Java, our visitor could visit a whole bunch of types, all implementing the Visited interface, and then the proper visit method would be dispatched to.

Ruby doesn't have a mechanism like this, so I thought I'd interpret what that might look like.

# Just an easy way to turn any enumerable object into
# a visited object without much code
module Visited
  def accept(visitor)
    each { |element| visitor.visit element }
  end
end


# The visitor here gives us the power to respond to any given type
# by simply telling it which type we want to respond to, and passing
# the block which does this responding
class Visitor
  def initialize
    yield self if block_given?
  end
  
  def visit(data)
    type = data.class
    return handler[type][data] if handler[type]
    default && default[data]
  end
  
  def handler
    @handler ||= {}
  end
  
  def implement_for(type, &block)
    handler[type] = block
  end
  
  def default_implementation(&block)
    @default = block
  end
  
  attr_reader :default
end


# Okay, so lets define our visitor, It'll just record all the types it saw
result = ""
visitor = Visitor.new do |v|
  v.implement_for Fixnum do |num|
    result << "(Fixnum #{num})"
  end
  
  v.implement_for String do |string|
    result << "(String #{string.inspect})"
  end
  
  v.implement_for Symbol do |sym|
    result << "(Symbol #{sym.inspect})"
  end
  
  v.default_implementation do |element|
    result << "(Unknown #{element.inspect})"
  end
end


# And we'll visit an array
ary = [12, "abc", :def, 9, /abcdefgh/i].extend Visited
ary.accept visitor
result # => "(Fixnum 12)(String \"abc\")(Symbol :def)(Fixnum 9)(Unknown /abcdefgh/i)"


# And we'll visit a set
result.clear
require 'set'
set = Set['a', :b, 8].extend Visited
set.accept visitor
result # => "(String \"a\")(Symbol :b)(Fixnum 8)"

Arrays and sets aren't quite as interesting as trees, but I didn't want to clutter the code. The thing to take away is that we can iterate over this Visited, and respond to each of its types without having to go edit existing methods.

The biggest difference between my implementation and the original idea is that mine doesn't understand inheritance. You could change that, but it would require giving up hash access.

Anyway, I have at least one interesting idea in my mind for how I'd enjoy using something like this. The basic idea is you pass the visitor to a bunch of objects which then define their own implementation on it. Then it doesn't have to even know about what kinds of objects it can visit. Each vistor just decides what it's interested in and so it is. You could even allow for multiple objects to implement a visitor for a given class. Anyone wishing to visit any of the objects just identifies which objects they want to visit, and their block will be called when it is seen. The visitor just travels along, passing the objects to each of the responders as appropriate. I'll keep it tucked away in case the situation ever arises :).

blog comments powered by Disqus