Command

Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

Also known as: Action, Transaction

Participants

RoleIn this exampleResponsibility
Command#execute, #undoThe interface every command shares.
Concrete CommandPlaceTower, UpgradeTowerBinds a receiver to an action and remembers what's needed to reverse it.
ReceiverBoardThe object that actually changes.
InvokerControlsRuns commands and keeps the undo and redo history. It never knows what any command does.

Ruby implementation

command.rb
# The Receiver: the board the commands change.
class Board
  attr_accessor :gold
  attr_reader :towers

  def initialize(gold:)
    @gold = gold
    @towers = {}
  end
end

# Commands: each player action is an object that can do and undo itself.
class PlaceTower
  COST = 50

  def initialize(board, cell)
    @board = board
    @cell = cell
  end

  def execute
    @board.gold -= COST
    @board.towers[@cell] = 1
  end

  def undo
    @board.towers.delete(@cell)
    @board.gold += COST
  end
end

class UpgradeTower
  COST = 40

  def initialize(board, cell)
    @board = board
    @cell = cell
  end

  def execute
    @board.gold -= COST
    @board.towers[@cell] += 1
  end

  def undo
    @board.towers[@cell] -= 1
    @board.gold += COST
  end
end

# The Invoker: runs commands and keeps the history needed to undo and redo.
# It never knows what any command does.
class Controls
  def initialize
    @done = []
    @undone = []
  end

  def run(command)
    command.execute
    @done.push(command)
    @undone.clear
    self
  end

  def undo
    command = @done.pop or return self
    command.undo
    @undone.push(command)
    self
  end

  def redo
    command = @undone.pop or return self
    command.execute
    @done.push(command)
    self
  end
end

In use

board    = Board.new(gold: 120)
controls = Controls.new

controls.run(PlaceTower.new(board, [3, 2]))
        .run(UpgradeTower.new(board, [3, 2]))

board.towers  # => {[3, 2]=>2}
board.gold    # => 30

controls.undo
board.towers  # => {[3, 2]=>1}
board.gold    # => 70

Once each key press makes a command instead of calling the game directly, undo, replays and rebindable keys all have somewhere to live.

Notes

Ruby idioms

  • A lambda is a command with no undo. For simple callbacks, -> { ... } and #call are enough. Write a class when you need #undo, a name in a log, or serialisation for a job queue.
  • command = @done.pop or return self uses Ruby’s low-precedence or to bail out of an empty history in one line.
  • Background jobs (Active Job, Sidekiq) are commands: a request captured as an object and run later, somewhere else.

Reach for it when

  • You need undo and redo, macros, replays, or a log of what happened.
  • A request must be queued, scheduled, retried or sent to another process.

Watch for

  • Undo is only as good as what each command remembers. If upgrading also changed a tower’s targeting, UpgradeTower would have to store the old targeting to put it back.

Requests become objects, so they can be stored, replayed and undone.