Alex Rivera | Logout

Visitor Pattern in Ruby, or just use a Block?

Asked 2009-10-05T06:10:03.447
11

Hey there, I have read the few posts here on when/how to use the visitor pattern, and some articles/chapters on it, and it makes sense if you are traversing an AST and it is highly structured, and you want to encapsulate the logic into a separate "visitor" object, etc. But with Ruby, it seems like overkill because you could just use blocks to do nearly the same thing.

I would like to pretty_print xml using Nokogiri. The author recommended that I use the visitor pattern, which would require I create a FormatVisitor or something similar, so I could just say "node.accept(FormatVisitor.new)".

The issue is, what if I want to start customizing all the stuff in the FormatVisitor (say it allows you to specify how nodes are tabbed, how attributes are sorted, how attributes are spaced, etc.).

  • One time I want the nodes to have 1 tab for each nest level, and the attributes to be in any order
  • The next time, I want the nodes to have 2 spaces, and the attributes in alphabetical order
  • The next time, I want them with 3 spaces and with two attributes per line.

I have a few options:

  • Create an options hash in the constructor (FormatVisitor.new({:tabs => 2})
  • Set values after I have constructed the Visitor
  • Subclass the FormatVisitor for each new implementation
  • Or just use blocks, not the visitor

Instead of having to construct a FormatVisitor, set values, and pass it to the node.accept method, why not just do this:


node.pretty_print do |format|
  format.tabs = 2
  format.sort_attributes_by {...}
end

That's in contrast to what I feel like the visitor pattern would look like:


visitor = Class.new(FormatVisitor) do
  attr_accessor :format
  def pretty_print(node)
    # do something with the text
    @format.tabs = 2 # two tabs per nest level
    @format.sort_attributes_by {...}
  end
end.new
Edit
Report

1 Answer

17

In essence, a Ruby block is the Visitor pattern without the extra boilerplate. For trivial cases, a block is sufficient.

For example, if you want to perform a simple operation on an Array object, you would just call the #each method with a block instead of implementing a separate Visitor class.

However, there are advantages in implementing a concrete Visitor pattern under certain cases:

  • For multiple, similar but complex operations, Visitor pattern provides inheritance and blocks don't.
  • Cleaner to write a separate test suite for Visitor class.
  • It's always easier to merge smaller, dumb classes into a larger smart class than separating a complex smart class into smaller dumb classes.

Your implementation seems mildly complex, and Nokogiri expects a Visitor instance that impelment #visit method, so Visitor pattern would actually be a good fit in your particular use case. Here is a class based implementation of the visitor pattern:

FormatVisitor implements the #visit method and uses Formatter subclasses to format each node depending on node types and other conditions.

# FormatVisitor implments the #visit method and uses formatter to format
# each node recursively.
class FormatVistor

  attr_reader :io

  # Set some initial conditions here.
  # Notice that you can specify a class to format attributes here.
  def initialize(io, tab: "  ", depth: 0, attributes_formatter_class: AttributesFormatter)
    @io = io
    @tab = tab
    @depth = depth
    @attributes_formatter_class = attributes_formatter_class
  end

  # Visitor interface. This is called by Nokogiri node when Node#accept
  # is invoked.
  def visit(node)
    NodeFormatter.format(node, @attributes_formatter_class, self)
  end

  # helper method to return a string with tabs calculated according to depth
  def tabs
    @tab * @depth
  end

  # creates and returns anothe
answered 2012-03-22T18:56:57.550

Your Answer