最近讀書會在讀深入淺出設計模式,趁這個機會複習一下設計模式,試著舉出簡單的例子並且用非模式與模式的方式來實作,比較它們的差異與優缺點。
玩具工廠製造了三種機器人,分別為走路機器人(WalkingRobot)、坦克機器人(TankRobot)與鴨鴨機器人(DuckRobot),每個機器人都有各自的動作如下:
程式碼如下所示:
class WalkingRobot
def walk(direction)
p "Walk #{direction}"
end
def turn(direction)
p "Turn #{direction}"
end
end
class TankRobot
def move_left_track(direction)
p "Move left track #{direction}"
end
def move_right_track(direction)
p "Move right track #{direction}"
end
end
class DuckRobot
def swim(direction)
p "Swim #{direction}"
end
def move_oar(direction)
p "Move the oar to #{direction}"
end
end
現在玩具工廠想做一個遙控器來控制機器人,遙控器有三個功能:控制向前移動(move)、右轉(turn_right)與左轉(turn_left),要特別注意的是每種機器人都有各自移動與轉方向的方式:
另外要針對三種機器人都各做一隻遙控器成本太高,所以希望能用同一隻通用遙控器(UniversalControl)就可以支援這三種機器人的控制,只要在一開始設定的時候指定要控制哪一種機器人就可以了。
class UniversalControl
def robot=(robot)
@robot = robot
end
def move
if @robot.is_a?(WalkingRobot)
@robot.walk(:forward)
elsif @robot.is_a?(TankRobot)
@robot.move_left_track(:forward)
@robot.move_right_track(:forward)
elsif @robot.is_a?(DuckRobot)
@robot.swim(:forward)
else
fail 'Not a supported robot!!'
end
end
def turn_right
if @robot.is_a?(WalkingRobot)
@robot.turn(:right)
elsif @robot.is_a?(TankRobot)
@robot.move_left_track(:forward)
@robot.move_right_track(:backward)
elsif @robot.is_a?(DuckRobot)
@robot.move_oar(:right)
@robot.swim(:forward)
else
fail 'Not a supported robot!!'
end
end
def turn_left
if @robot.is_a?(WalkingRobot)
@robot.turn(:left)
elsif @robot.is_a?(TankRobot)
@robot.move_left_track(:backward)
@robot.move_right_track(:forward)
elsif @robot.is_a?(DuckRobot)
@robot.move_oar(:left)
@robot.swim(:forward)
else
fail 'Not a supported robot!!'
end
end
end
control = UniversalControl.new
walking_robot = WalkingRobot.new
control.robot = walking_robot
control.move
control.turn_right
control.turn_left
tank_robot = TankRobot.new
control.robot = tank_robot
control.move
control.turn_right
control.turn_left
duck_robot = DuckRobot.new
control.robot = duck_robot
control.move
control.turn_right
control.turn_left
class RobotCommand
def initialize(robot)
@robot = robot
end
def execute
fail 'You should implement "execute" method in your RobotCommand-based class.'
end
end
class WalkingMoveCommand < RobotCommand
def execute
@robot.walk(:forward)
end
end
class WalkingTurnRightCommand < RobotCommand
def execute
@robot.turn(:right)
end
end
class WalkingTurnLeftCommand < RobotCommand
def execute
@robot.turn(:left)
end
end
class TankMoveCommand < RobotCommand
def execute
@robot.move_left_track(:forward)
@robot.move_right_track(:forward)
end
end
class TankTurnRightCommand < RobotCommand
def execute
@robot.move_left_track(:forward)
@robot.move_right_track(:backward)
end
end
class TankTurnLeftCommand < RobotCommand
def execute
@robot.move_left_track(:backward)
@robot.move_right_track(:forward)
end
end
class DuckMoveCommand < RobotCommand
def execute
@robot.swim(:forward)
end
end
class DuckTurnRightCommand < RobotCommand
def execute
@robot.move_oar(:right)
@robot.swim(:forward)
end
end
class DuckTurnLeftCommand < RobotCommand
def execute
@robot.move_oar(:left)
@robot.swim(:forward)
end
end
class UniversalControl
def initialize
@commands = {}
end
def setup(type, command)
@commands[type] = command
end
def move
@commands[:move].execute if @commands[:move] != nil
end
def turn_right
@commands[:turn_right].execute if @commands[:turn_right] != nil
end
def turn_left
@commands[:turn_left].execute if @commands[:turn_left] != nil
end
end
control = UniversalControl.new
walking_robot = WalkingRobot.new
control.setup(:move, WalkingMoveCommand.new(walking_robot))
control.setup(:turn_right, WalkingTurnRightCommand.new(walking_robot))
control.setup(:turn_left, WalkingTurnLeftCommand.new(walking_robot))
control.move
control.turn_right
control.turn_left
tank_robot = TankRobot.new
control.setup(:move, TankMoveCommand.new(tank_robot))
control.setup(:turn_right, TankTurnRightCommand.new(tank_robot))
control.setup(:turn_left, TankTurnLeftCommand.new(tank_robot))
control.move
control.turn_right
control.turn_left
duck_robot = DuckRobot.new
control.setup(:move, DuckMoveCommand.new(duck_robot))
control.setup(:turn_right, DuckTurnRightCommand.new(duck_robot))
control.setup(:turn_left, DuckTurnLeftCommand.new(duck_robot))
control.move
control.turn_right
control.turn_left
我們希望可以在遙控器上加一個復原的按鈕,當按鈕按下去的時候可以復原前一個命令。這時候只要在command中定義相對應的undo method,在按鈕按下去的時候去呼叫對應的undo就ok了。
class RobotCommand
def initialize(robot)
@robot = robot
end
def execute
fail 'You should implement "execute" method in your RobotCommand-based class.'
end
def undo
fail 'You should implement "undo" method in your RobotCommand-based class.'
end
end
class WalkingMoveCommand < RobotCommand
def execute
@robot.walk(:forward)
end
def undo
@robot.walk(:backward)
end
end
class WalkingTurnRightCommand < RobotCommand
def execute
@robot.turn(:right)
end
def undo
@robot.turn(:left)
end
end
class WalkingTurnLeftCommand < RobotCommand
def execute
@robot.turn(:left)
end
def undo
@robot.turn(:right)
end
end
class TankMoveCommand < RobotCommand
def execute
@robot.move_left_track(:forward)
@robot.move_right_track(:forward)
end
def undo
@robot.move_left_track(:backward)
@robot.move_right_track(:backward)
end
end
class TankTurnRightCommand < RobotCommand
def execute
@robot.move_left_track(:forward)
@robot.move_right_track(:backward)
end
def undo
@robot.move_left_track(:backward)
@robot.move_right_track(:forward)
end
end
class TankTurnLeftCommand < RobotCommand
def execute
@robot.move_left_track(:backward)
@robot.move_right_track(:forward)
end
def undo
@robot.move_left_track(:forward)
@robot.move_right_track(:backward)
end
end
class DuckMoveCommand < RobotCommand
def execute
@robot.swim(:forward)
end
def undo
@robot.swim(:backward)
end
end
class DuckTurnRightCommand < RobotCommand
def execute
@robot.move_oar(:right)
@robot.swim(:forward)
end
def undo
@robot.move_oar(:right)
@robot.swim(:backward)
end
end
class DuckTurnLeftCommand < RobotCommand
def execute
@robot.move_oar(:left)
@robot.swim(:forward)
end
def undo
@robot.move_oar(:left)
@robot.swim(:backward)
end
end
class UniversalControl
def initialize
@commands = {}
end
def setup(type, command)
@commands[type] = command
end
def move
@commands[:move].execute if @commands[:move] != nil
@last_command = :move
end
def turn_right
@commands[:turn_right].execute if @commands[:turn_right] != nil
@last_command = :turn_right
end
def turn_left
@commands[:turn_left].execute if @commands[:turn_left] != nil
@last_command = :turn_left
end
def undo
@commands[@last_command].undo if @commands[@last_command] != nil
end
end
control = UniversalControl.new
walking_robot = WalkingRobot.new
control.setup(:move, WalkingMoveCommand.new(walking_robot))
control.setup(:turn_right, WalkingTurnRightCommand.new(walking_robot))
control.setup(:turn_left, WalkingTurnLeftCommand.new(walking_robot))
control.move
control.undo
control.turn_right
control.undo
使用命令模式的另一個好處是可以定義批次命令,例如我們可以建一個 BatchCommand class 如下:
class BatchCommand
def initialize(commands)
@commands = commands
end
def execute
@commands.each do |command|
command.execute
end
end
def undo
@commands.reverse.each do |command|
command.undo
end
end
end
這時候我們可以利用 BatchCommand 來組合多個 command,例如我們想要讓走路機器人的move是往前走三步,而不是只有一步,則我們可以設定遙控器如下:
control = UniversalControl.new
walking_robot = WalkingRobot.new
move_command = WalkingMoveCommand.new(walking_robot)
boost_command = BatchCommand.new([
move_command,
move_command,
move_command
])
control.setup(:move, boost_command)
control.setup(:turn_right, WalkingTurnRightCommand.new(walking_robot))
control.setup(:turn_left, WalkingTurnLeftCommand.new(walking_robot))
control.move
control.undo
control.turn_right
control.undo
Command Pattern - 命令模式
使用中介的 class(RobotCommand系列的class) 做為傳遞命令的媒界,使呼叫命令的 invoker(UniversalControl) 與實際執行的 receiver(WalkingRobot, TankRobot, DuckRobot) 之間的相依性降低。另外藉由共同實作的 execute method,讓 invoker(UniversalControl) 可以動態切換要執行的命令,而不用了解執行命令的 receiver(WalkingRobot, TankRobot, DuckRobot) 是哪個 class,同時也可以做到復原(undo)與批次(batch)執行的功能。