Last active
October 18, 2024 16:07
-
-
Save jhbabon/98bcaaf91720be312254 to your computer and use it in GitHub Desktop.
Visitor pattern (double dispatch) on Ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Visitable | |
def accept(visitor) | |
visitor.visit self | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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