General
Gems
- Seeing Is Believing
- Surrogate
- Mountain Berry Fields
- Deject
- Pbcopy
- LetterPress Is Not As Good As Boggle
- Haiti
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 .