Double Dispatch
Objective: Turn a type checking imperative procedure in a more maintenable OO version.
Related to Visitor Pattern.
Some differences between the two on Refactoring Guru
Applications
- Avoiding
case
for type checking in Ruby to comply with polymorphic collections - Use the Visitor when you need to perform an operation on all elements of a complex object structure (trasversing a tree). Like Visitor
One Example
Using a simple game, Rock, Paper, Scissors, like in RubyGuides we can start understand why:
class Scissors
def wins_against?(other_move)
case other_move
when Rock then false
when Paper then true
end
end
end
We want to avoid that example of case with this:
class Scissors
def wins_against?(other_move)
other_move.do_you_beat_scissors?
end
def do_you_beat_rock?
false
end
def do_you_beat_paper?
true
end
end
Another example
# Hosts
class Car
def dispatch_maintenance(caretaker)
caretaker.maintenance_on_car(self)
end
end
class Truck
def dispatch_maintenance(caretaker)
caretaker.maintenance_on_truck(self)
end
end
# Visitors
class Mechanic
def work_on(vehicle)
vehicle.dispatch_maintenance(self)
end
def maintenance_on_car(vehicle)
change_motor_oil(vehicle, tools: LightTools.grab())
end
def maintenance_on_truck(vehicle)
change_motor_oil(vehicle, tools: HeavyTools.grab())
check_tires_condition(vehicle) && change_tires(vehicle)
end
end
class CarWasher
def work_on(vehicle)
vehicle.dispatch_work(self)
end
def maintenance_on_car(vehicle)
grab_some_cleaning_cloth()
do_wash(vehicle, using: LightMachine)
end
def maintenance_on_truck(vehicle)
check_water_reservoir()
do_wash(vehicle, using: HeavyMachine)
end
end
mechanic = Mechanic.new
mechanic.work_on(vw_golf)
Here we see caretakers [Mechanic, CarWasher]
visiting the patients instances [Car, Truck]
. The hosts are responsible to dispatch the correct action for caretakers. It's odd on reality of this example.
Using a in View
Now from Sandi Metz's blog.
class MyView
attr_reader :target
def initialize(target)
@target = target
end
def double
case target
when Numeric then target * 2
when String then target.next # lazy example fail
when Array then target.collect {|o| MyView.new(o).double}
else
raise “don’t know how to double #{target.class} #{target.inspect}”
end
end
end
class Numeric
def double
self * 2
end
end
class String
def double
self.next
end
end
class Array
def double
collect {|e| e.double}
end
end
class Object
def double
raise "don't know how to double #{self.class} #{self.inspect}"
end
end
class MyView
attr_reader :target
def initialize(target)
@target = target
end
def double
target.double
end
end