Command Pattern

Posted About 7 years ago. Visible to the public.

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
Alexander M
Last edit
About 7 years ago
Alexander M
Posted by Alexander M to Ruby and RoR knowledge base (2017-01-18 10:35)