Skip to content

Instantly share code, notes, and snippets.

@benolee
Forked from tenderlove/as_array.rb
Created December 12, 2013 21:23
Show Gist options
  • Save benolee/7935706 to your computer and use it in GitHub Desktop.
Save benolee/7935706 to your computer and use it in GitHub Desktop.
double dispatch
Link: [1]RSS (alternate)
NOTE:This blog had a good run, but is now in retirement.
If you enjoy the content here, please support Gregory's ongoing work on
the [2]Practicing Ruby journal.
[3]Gregory Brown [4]James Britt [5]Robert Klemme [6]Magnus Holm
[7]Ruby Best Practices
The Double Dispatch Dance
2009-06-19 20:30, written by [8]Aaron Patterson
EDITORIAL NOTE: This is the first of hopefully many guest posts to the RBP
blog. If you are interested in writing an article for us, contact any of
the RBP bloggers for details on how to submit content.
[9]Sandi Metz recently blogged [10]about ruby case statements being
similar to calling is_a? on a particular object, and how we should be
using duck typing rather than inspecting the type of the class. Her
solution was to add methods to some core classes so that they all
"quacked" the same way.
I completely agree with Sandi's sentiments, but at the same time, I don't
like reopening core classes, so I wanted to see if I could accomplish the
same task, but minimize my impact on any core classes. The way I chose to
accomplish this task is with the [11]double dispatch and [12]visitor
pattern .
Double Dispatch
The way the double dispatch pattern works is by adding a method to each
object that should know how to double dispatch. That method takes one
argument, and calls a method on the argument passing itself in. In my
solution, I called the method "accept", and I want all objects to do
double dispatch, so I've added that method to Object:
##
# The visitable module makes a class "visitable". It follows the double
# dispatch pattern and simply calls "visit" on the argument with itself.
module Visitable
def accept visitor
visitor.visit self
end
end
##
# For this example, we want to make *all* objects visitable, so we'll mix
# the Visitable module in to Object.
class Object
include Visitable
end
Excellent, but what does this buy me? It gives me an entry point and
common interface for dispatching a method based on the object I am
visiting. I just implement an object that responds to "visit" and that
methods knows how to dispatch.
Our Dispatching Visitor
I've implemented a class that responds to "visit", and based on the type
of the object, dispatches to a particular method. If a string is passed in
to the "visit" method, it will try to dispatch the object to
"visit_String". If "visit_String" is not defined, it will try look at the
string's ancestor list trying to find a place to dispatch. If it can't
dispatch, an exception is raised. My Visitor base class also has a helper
method "visitor_for" to help define handlers.
##
# The Visitor class is our base class for all visitors. It implements the
# "visit" method which Object#accept will call.
class Visitor
###
# Dynamically create a visitor method for each class in +klasses+
def self.visitor_for *klasses, &block
klasses.each do |klass|
define_method(:"visit_#{klass.name}", block)
end
end
##
# This method will examine the class and ancestors of +thing+. For each
# class in the "ancestors" list, it will check to see if the visitor knows
# how to handle that particular class. If it can't find a handler for the
# +thing+ it will raise an exception.
def visit thing
thing.class.ancestors.each do |ancestor|
method_name = :"visit_#{ancestor.name}"
next unless respond_to? method_name
return send method_name, thing
end
raise "Can't handle #{thing.class}"
end
end
My base Visitor class is not really of much use at this point. All it
knows how to do is accept an object and try to send a method based on the
type. Let's take a look at a visitor that will convert any object to an
array.
Array Conversion Visitor
One of the tasks is to convert any object in to an array. I can write a
Visitor class that has one responsibility; turning any object in to an
Array. The rules for this conversion are easy. If the object is an array,
return the array, if it's something else, return that thing inside an
array:
require 'visitor'
##
# This Visitor knows how to turn any object in to an Array
class AsArray < Visitor
visitor_for Array do |array|
array
end
visitor_for Object do |o|
[o]
end
end
That was pretty easy. If the target of the "accept" method is an array,
the visit_Array method will be called an the target object will be
returned. If it's something else, visit_Object will be called and a new
object will be created.
I can use this style pattern for converting any object to pretty much any
other.
Cleaning up our API
So far my API is kind of strange. I have to call "accept" on the target
object and pass in new visitor of some sort. To clean this API up a bit,
I've written a proxy object that takes any object for it's constructor,
then provides nice method names to abstract the visitor classes:
require 'visitable'
require 'visitor'
require 'as_form_object'
require 'as_array'
require 'as_fields_for_form_object'
###
# This class sets up a proxy to +thing+. The purpose of this class is to let
# us convert an object to something else without knowing what visitor is
# required to do that conversion:
#
# p form_for([1, :two, [3]]).as_form_object
#
class ConversionProxy < Struct.new(:thing)
def as_form_object
thing.accept AsFormObject.new
end
def as_array
thing.accept AsArray.new
end
def as_fields_for_form_object args
thing.accept AsFieldsForFormObject.new(args)
end
end
def form_for thing
ConversionProxy.new(thing)
end
p form_for(:two).as_form_object
p form_for(:two).as_array
p form_for([:two]).as_array
p form_for(:two).as_fields_for_form_object [:foo]
p form_for(['hello world']).as_fields_for_form_object [:foo]
Pros for this solution
With one monkey patch to Object, I now have the ability to convert any
object in to any other object without needing to add new methods to core
classes.
Each visitor has one specific responsibility. My visitor classes are just
normal classes, so if I find any repetition between them, I can refactor
and inherit, or refactor and mix in.
Cons for this solution
I still had to monkey patch Object and add a method. The method I added
was very generic and simple, but I still had to add it.
Visitation rules may need to change. My current behavior of looking up the
ancestor tree may not be desired. Fortunately if I need to change that, my
visitor subclass can just override visit.
Double dispatch might be confusing. The code is very simple, but it took
me a while to understand and harness it's power (I have the
pooowwwweeerrr!).
Is this any better than case/when statements? I'm not sure. It still works
based on object type, but it does nicely encapusulate the conversion
logic.
Follow up notes
When I'm implementing this pattern in "the real world" I've never done it
for core classes. I typically use this for converting my own objects in to
some other object. Since I'm performing these actions on my own objects,
there is no monkey pathing involved (yay!).
Also, its worth keeping in mind that babbies can form in many ways, and
this is only one of them. Tune in next week to watch Greg tackle the same
problem using Decorators, to see the pros and cons from yet another angle.
[13]View the discussion thread.[14]blog comments powered by Disqus
All content is under Copyright (C) 2009-2011 by the individual authors of
this blog and is released under the [15]Attribution-ShareAlike 3.0
Unported license. If you have any questions, please contact the author of
the material you are interested in using.
References
Visible links
1. http://blog.rubybestpractices.com/feed.xml
2. http://practicingruby.com/
3. file:///about/gregory.html
4. file:///about/jamesbritt.html
5. file:///about/rklemme.html
6. file:///about/judofyr.html
7. http://blog.rubybestpractices.com/
8. file:///about/aaronp.html
9. http://sandimetz.com/
10. http://sandimetz.com/2009/06/ruby-case-statements-and-kindof.html
11. http://en.wikipedia.org/wiki/Double_dispatch
12. http://en.wikipedia.org/wiki/Visitor_pattern
13. http://rbp-blog.disqus.com/?url=ref
14. http://disqus.com/
15. http://creativecommons.org/licenses/by-sa/3.0/
require 'visitor'
##
# This Visitor knows how to turn any object in to an Array
class AsArray < Visitor
visitor_for Array do |array|
array
end
visitor_for Object do |o|
[o]
end
end
require 'visitor'
###
# This Visitor knows how to do stuff with arguments based on class type.
class AsFieldsForFormObject < Visitor
def initialize args
@args = args
end
##
# If the recipient is a String or Symbol, we just want to return the first
# from @arg
visitor_for String, Symbol do |thing|
@args.first
end
###
# If it is an object, we want to get it as a form object
visitor_for Object do |thing|
form_for(thing).as_form_object
end
end
require 'visitor'
##
# This class knows how to turn any object in to a form object.
class AsFormObject < Visitor
##
# If it is a String, Symbol, Fixnum, or Object just return the thing
visitor_for String, Symbol, Fixnum do |thing|
thing
end
##
# If it's an Array, we just want the last bit of the array, and we'll convert
# it to a form object as well
visitor_for Array do |list|
list.last.accept self
end
end
require 'visitable'
require 'visitor'
require 'as_form_object'
require 'as_array'
require 'as_fields_for_form_object'
###
# This class sets up a proxy to +thing+. The purpose of this class is to let
# us convert an object to something else without knowing what visitor is
# required to do that conversion:
#
# p form_for([1, :two, [3]]).as_form_object
#
class ConversionProxy < Struct.new(:thing)
def as_form_object
thing.accept AsFormObject.new
end
def as_array
thing.accept AsArray.new
end
def as_fields_for_form_object args
thing.accept AsFieldsForFormObject.new(args)
end
end
def form_for thing
ConversionProxy.new(thing)
end
p form_for(:two).as_form_object
p form_for(:two).as_array
p form_for([:two]).as_array
p form_for(:two).as_fields_for_form_object [:foo]
p form_for(['hello world']).as_fields_for_form_object [:foo]
##
# The visitable module makes a class "visitable". It follows the double dispatch
# pattern and simply calls "visit" on the argument with itself.
module Visitable
def accept visitor
visitor.visit self
end
end
##
# For this example, we want to make *all* objects visitable, so we'll mix the
# Visitable module in to Object.
class Object
include Visitable
end
##
# The Visitor class is our base class for all visitors. It implements the
# "visit" method which Object#accept will call.
class Visitor
###
# Dynamically create a visitor method for each class in +klasses+
def self.visitor_for *klasses, &block
klasses.each do |klass|
define_method(:"visit_#{klass.name}", block)
end
end
##
# This method will examine the class and ancestors of +thing+. For each
# class in the "ancestors" list, it will check to see if the visitor knows
# how to handle that particular class. If it can't find a handler for the
# +thing+ it will raise an exception.
def visit thing
thing.class.ancestors.each do |ancestor|
method_name = :"visit_#{ancestor.name}"
next unless respond_to? method_name
return send method_name, thing
end
raise "Can't handle #{thing.class}"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment