Memento
Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.
Also known as: Token
Participants
| Role | In this example | Responsibility |
|---|---|---|
| Originator | Game | Creates snapshots of its own state and restores from them. |
| Memento | Game::Snapshot | A frozen record of the game's state. A private constant, so only Game names it. |
| Caretaker | Checkpoints | Stores snapshots and hands them back, without looking inside. |
Ruby implementation
# The Originator: a game that can save and restore its own state.
class Game
# The Memento: a frozen snapshot. Only Game creates or reads it.
Snapshot = Data.define(:wave, :gold, :lives, :towers)
private_constant :Snapshot
attr_reader :wave, :gold, :lives, :towers
def initialize(gold:, lives:)
@wave = 0
@gold = gold
@lives = lives
@towers = {}
end
def build(kind, cell, cost:)
@gold -= cost
@towers[cell] = kind
self
end
def start_wave
@wave += 1
self
end
def lose_lives(count)
@lives -= count
self
end
def save
Snapshot.new(wave:, gold:, lives:, towers: towers.dup.freeze)
end
def restore(snapshot)
@wave = snapshot.wave
@gold = snapshot.gold
@lives = snapshot.lives
@towers = snapshot.towers.dup
self
end
end
# The Caretaker: keeps snapshots without ever looking inside them.
class Checkpoints
def initialize
@snapshots = []
end
def record(snapshot)
@snapshots.push(snapshot)
self
end
def latest = @snapshots.last
def count = @snapshots.size
endIn use
game = Game.new(gold: 120, lives: 20)
checkpoints = Checkpoints.new
game.build(:arrow, [3, 2], cost: 50)
checkpoints.record(game.save) # before wave 1
game.start_wave.lose_lives(15)
game.restore(checkpoints.latest) # retry the wave
game.lives # => 20
game.towers # => {[3, 2]=>:arrow}
Notes
Ruby idioms
Data.definemakes a snapshot immutable by construction. Frozen, with no setters, it can’t drift from the moment it was taken.- Copy collections in and out.
towers.dup.freezeon save, and.dupon restore, so neither the snapshot nor the restored game can change the other. private_constant :Snapshotstops outside code from naming or building snapshots. Ruby can’t make the snapshot’s readers private to one class, so this is as sealed as it practically gets.
Reach for it when
- You need undo, checkpoints, “retry this wave”, or save and load.
- Exposing the object’s fields just so something else can save them would break its encapsulation.
Watch for
- Snapshots of large objects cost memory. Cap the caretaker’s history, or store changes instead (Command’s undo).
The caretaker stores snapshots but never looks inside them.