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
| Role | In this example | Responsibility |
|---|---|---|
| Command | #execute, #undo | The interface every command shares. |
| Concrete Command | PlaceTower, UpgradeTower | Binds a receiver to an action and remembers what's needed to reverse it. |
| Receiver | Board | The object that actually changes. |
| Invoker | Controls | Runs commands and keeps the undo and redo history. It never knows what any command does. |
Ruby implementation
# 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
endIn 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#callare enough. Write a class when you need#undo, a name in a log, or serialisation for a job queue. command = @done.pop or return selfuses Ruby’s low-precedenceorto 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,
UpgradeTowerwould have to store the old targeting to put it back.
Requests become objects, so they can be stored, replayed and undone.