Skip to content

Instantly share code, notes, and snippets.

@jhbabon
Last active October 18, 2024 16:07
Show Gist options
  • Save jhbabon/98bcaaf91720be312254 to your computer and use it in GitHub Desktop.
Save jhbabon/98bcaaf91720be312254 to your computer and use it in GitHub Desktop.
Visitor pattern (double dispatch) on Ruby
require_relative 'visitor'
require_relative 'visitable'
class Object
include Visitable
end
class Log < Visitor
def visit_Array(array)
puts 'Logging array:'
array.each_with_index do |item, index|
puts "#{index}: #{item}"
end
end
def visit_Object(object)
puts "Logging object: #{object.inspect}"
end
def visit_class_Object(klass)
puts "Logging class: #{klass}"
end
end
class Doubler < Visitor
def visit_Array(array)
array.map { |n| n * 2 }
end
end
log = Log.new
doubler = Doubler.new
array = [1, 2, 3]
array.accept log
#=> Logging array:
#=> 0: 1
#=> 1: 2
#=> 2: 3
array.accept(doubler).accept(log)
#=> Logging array:
#=> 0: 2
#=> 1: 4
#=> 2: 6
symbol = :a
symbol.accept log
#=> Logging object: :a
Symbol.accept log
#=> Logging class: Symbol
symbol.accept doubler
#=> visitor.rb:116:in `dispatch_visit': Can't visit Symbol (TypeError)
#=> from visitor.rb:105:in `visit_on_instance'
#=> from visitor.rb:95:in `visit'
#=> from visitable.rb:3:in `accept'
#=> from test.rb:42:in `<main>'
module Visitable
def accept(visitor)
visitor.visit self
end
end
# Base Visitor class to implement any kind of Visitor
#
# Ideas taken from http://blog.rubybestpractices.com/posts/aaronp/001_double_dispatch_dance.html and
# http://blog.bigbinary.com/2013/07/07/visitor-pattern-and-double-dispatch.html
#
# A Visitor knows how to make actions based on the type
# of an Object.
#
# In order to make a visit method for a concrete type,
# you need to declare the method with the type signature, like this:
#
# # For Type objects
# def visit_Type(instance)
# # magic
# end
#
# # For ActiveRecord::Base objects
# def visit_ActiveRecord_Base(record)
# # magic
# end
#
# You can also make the Visitor to operate over classes instead of
# instances by using methods with the prefix visit_class, like this:
#
# def visit_class_Type(klass)
# end
#
# @see Visitable module
#
# @example Visitor to transform instances to Arrays
#
# class AsArray < Visitor
# def visit_Array(array)
# array
# end
#
# def visit_Object(object)
# [object]
# end
# end
#
# AsArray.new.visit [a, b]
# #=> [a, b]
#
# o = Object.new
# AsArray.new.visit o
# #=> [o]
#
# @example Visitor to create Arrays from their classes
#
# class AsArray < Visitor
# def visit_class_Array(array_class)
# array_class.new
# end
# end
#
# class MyArray < Array; end
#
# AsArray.new.visit MyArray
# #=> []
class Visitor
VISIT_REGEXP = /\Avisit_((?<class_level>class)_)?(?<type>\w+)/.freeze
# Look for any new visit_* method added and add it to the class
# dispatch tables for instances and classes.
def self.method_added(method_name)
return if visit_method_exist?(method_name)
VISIT_REGEXP.match(method_name.to_s) do |match|
if match[:class_level].nil?
dispatch_instance[match[:type]] = method_name
else
dispatch_class[match[:type]] = method_name
end
end
end
# Does the method already exists?
#
# @return [Boolean]
def self.visit_method_exist?(method_name)
dispatch_instance.value?(method_name) || dispatch_class.value?(method_name)
end
def self.dispatch_instance
@dispatch_instance ||= {}
end
def self.dispatch_class
@dispatch_class ||= {}
end
# Visit an object or class based on its Type.
#
# @param thing [Anything]
# @return [Anything]
def visit(thing)
thing.is_a?(Class) ? visit_on_class(thing) : visit_on_instance(thing)
end
private
def visit_on_class(thing)
dispatch_visit thing, self.class.dispatch_class, thing
end
def visit_on_instance(thing)
dispatch_visit thing.class, self.class.dispatch_instance, thing
end
def dispatch_visit(klass, dispatcher, *method_args)
klass.ancestors.each do |ancestor|
type = ancestor.name.gsub('::', '_')
method_name = dispatcher[type]
next unless method_name
return send(method_name, *method_args)
end
fail TypeError, "Can't visit #{klass}"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment