This pattern a simple but deceptively powerful technique that decouples events — such as user interface interactions — from the concrete actions those events should trigger. The command in the command pattern couldn’t be simpler: it is the combination of a thing and an action. It’s an object and a method to be invoked on that object. That’s it! By itself, a command doesn’t count as a Pattern™. It’s just another object. What makes the pattern is the “how” and the “why” it gets used. “How” command objects get used is via a set of similar objects. These invoker objects are usually (but are not limited to) UI elements—think buttons or menu items. And they need to be able to tell another object to do something. The pattern arises from a natural desire for consistency. You don’t want one button to call a function while another invokes a command via a call() method while a third uses a do() method. That’s silly. The common sense desire for all buttons to invoke their commands in the same way is what gives rise to the pattern. Consistency is not the sole benefit of the command pattern. Once you have a command object that can tell a series of objects to do something, it is a short distance to being able to tell those same objects to undo those actions. It also gets easier to combine actions into macro commands.
# Pattern name: receiver
class Robot
def initialize(x = 0, y = 0)
@x, @y, = x, y
end
def location
"x[#{@x}],y[#{@y}]"
end
def move_right
@x += 1
end
def move_left
@x -= 1
end
def move_up
@y += 1
end
def move_down
@y -= 1
end
end
# Pattern name: invoker
class Button
def initialize(name, command)
@name, @command = name, command
end
def press
@command.()
end
end
def start
puts '*** Robot Stuffs ***'
r = Robot.new
puts "Robot starts at: #{r.location}"
button_right = Button.new('Right', r.method(:move_right))
button_left = Button.new('Left', r.method(:move_left))
button_up = Button.new('Up', r.method(:move_up))
button_down = Button.new('Down', r.method(:move_down))
button_right.press
puts "Robot current location: #{r.location}"
button_left.press
puts "Robot current location: #{r.location}"
button_up.press
puts "Robot current location: #{r.location}"
button_down.press
puts "Robot current location: #{r.location}"
puts '---'
puts "Robot ends at: #{r.location}"
end
The fun stuff with the command pattern starts with undoing commands. After pressing the “Up” button, we ought to be able to hit the “Undo” button to return things to their previous state.
# Pattern name: command
module Command
def call
end
def undo
end
end
class MoveRight
include Command
def initialize(receiver)
@receiver = receiver
end
def call
@receiver.move_right
end
def undo
@receiver.move_left
end
end
class MoveLeft
include Command
def initialize(receiver)
@receiver = receiver
end
def call
@receiver.move_left
end
def undo
@receiver.move_right
end
end
class MoveUp
include Command
def initialize(receiver)
@receiver = receiver
end
def call
@receiver.move_up
end
def undo
@receiver.move_down
end
end
class MoveDown
include Command
def initialize(receiver)
@receiver = receiver
end
def call
@receiver.move_down
end
def undo
@receiver.move_up
end
end
class History
class << self
def stack
@stack ||= []
end
def add(command)
stack << command
end
def undo
command = stack.pop
command.undo
puts "Undoing #{command.class} command"
end
def undo_all
stack.each do |command|
command.undo
puts "Undoing #{command.class} command"
end
end
end
end
# Pattern name: invoker
class Button
def initialize(name, command)
@name, @command = name, command
end
def press
History.add @command
@command.call
end
end
def start
puts '*** Robot Stuffs ***'
r = Robot.new
puts "Robot starts at: #{r.location}"
move_right = MoveRight.new r
move_left = MoveLeft.new r
move_up = MoveUp.new r
move_down = MoveDown.new r
button_right = Button.new('Right', move_right)
button_left = Button.new('Left', move_left)
button_up = Button.new('Up', move_up)
button_down = Button.new('Down', move_down)
button_right.press
puts "Robot current location: #{r.location}"
button_right.press
puts "Robot current location: #{r.location}"
button_right.press
puts "Robot current location: #{r.location}"
button_left.press
puts "Robot current location: #{r.location}"
button_up.press
puts "Robot current location: #{r.location}"
button_down.press
puts "Robot current location: #{r.location}"
History.undo
History.undo_all
puts '---'
puts "Robot ends at: #{r.location}"
end