一個行程(Trip)在開始之前需要準備東西(prepare),根據不同的身份,要準備的東西也不同,例如:技工(Mechanic)必須要準備腳踏車(prepare_bicycles),行程規劃人員(TripCoordinator)要去買食物(buy_food),駕使(Driver)要將車子油(gas_up)與水(fill_water_tank)加滿。
class Trip
attr_reader :bicycles, :customers, :vehicle
def prepare(preparers)
preparers.each {|preparer|
case preparer
when Mechanic
preparer.prepare_bicycles(bicycles)
when TripCoordinator
preparer.buy_food(customers)
when Driver
preparer.gas_up(vehicle)
preparer.fill_water_tank(vehicle)
end
}
end
end
class Mechanic
def prepare_bicycles(bicycles)
bicycles.each {|bicycle| prepare_bicycle(bicycle)}
end
def prepare_bicycle(bicycle)
#...
end
end
class TripCoordinator
def buy_food(customers)
# ...
end
end
class Driver
def gas_up(vehicle)
#...
end
def fill_water_tank(vehicle)
#...
end
end
缺點:
Mechanic
的prepare_bicycles
改了它的名字,Trip
這個class的prepare
method就必須跟著改變。Trip
這個class的prepare
method也要跟著改變。改善方式:
prepare_trip
,Trip的prepare只需要呼叫prepare_trip即可。class Trip
attr_reader :bicycles, :customers, :vehicle
def prepare(preparers)
preparers.each do |preparer|
preparer.prepare_trip(self)
end
end
end
class Mechanic
def prepare_trip(trip)
trip.bicycles.each do |bicycle|
prepare_bicycle(bicycle)
end
end
# ...
end
class TripCoordinator
def prepare_trip(trip)
buy_food(trip.customers)
end
# ...
end
class Driver
def prepare_trip(trip)
vehicle = trip.vehicle
gas_up(vehicle)
fill_water_tank(vehicle)
end
# ...
end
多型(Polymorphism):同一個method在不同的class中有不同的實作。Duck Typing就是其中一種polymorphism的方式。
只要程式之中有下面三種情況,就要考慮是否要改用Duck Typing:
case preparer
when Mechanic
preparer.prepare_bicycles(bicycles)
when TripCoordinator
preparer.buy_food(customers)
when Driver
preparer.gas_up(vehicle)
preparer.fill_water_tank(vehicle)
end
kind_of?
或是is_a?
if preparer.kind_of?(Mechanic)
preparer.prepare_bicycles(bicycle)
elsif preparer.kind_of?(TripCoordinator)
preparer.buy_food(customers)
elsif preparer.kind_of?(Driver)
preparer.gas_up(vehicle)
preparer.fill_water_tank(vehicle)
end
responds_to?
if preparer.responds_to?(:prepare_bicycles)
preparer.prepare_bicycles(bicycle)
elsif preparer.responds_to?(:buy_food)
preparer.buy_food(customers)
elsif preparer.responds_to?(:gas_up)
preparer.gas_up(vehicle)
preparer.fill_water_tank(vehicle)
end
在某些情況下可以不使用Duck Typing,例如下面的例子:
def first(*args)
if args.any?
if args.first.kind_of?(Integer) ||
(loaded? && !args.first.kind_of?(Hash))
to_a.first(*args)
else
apply_finder_options(args.first).first
end
else
find_first
end
end
主要的原因是考量下面幾點:
Duck Typing有幾個缺點:
class MediaPlayer
def play(device)
device.play
end
def stop(device)
device.stop
end
end
class Dvd
def play
puts "play a movie"
end
def stop
puts "stop"
end
end
class Cd
def play
puts "play a music"
end
def stop
puts "stop"
end
end
mp = MediaPlayer.new
mp.play(Dvd.new)
mp.play(Cd.new)
mp.stop(Dvd.new)
mp.stop(Cd.new)
class MediaPlayer
def play(device)
device.play
end
def stop(device)
device.stop
end
end
module Device
def play
raise "Should implment the play method in your device."
end
def stop
puts "stop"
end
end
class Dvd
include Device
def play
puts "play a movie"
end
end
class Cd
include Device
def play
puts "play a music"
end
end
class WrongDevice
include Device
end
mp = MediaPlayer.new
mp.play(Dvd.new)
mp.play(Cd.new)
mp.stop(Dvd.new)
mp.stop(Cd.new)
# mp.play(WrongDevice.new) # raise an exception if play method is not implemented