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

RoleIn this exampleResponsibility
OriginatorGameCreates snapshots of its own state and restores from them.
MementoGame::SnapshotA frozen record of the game's state. A private constant, so only Game names it.
CaretakerCheckpointsStores snapshots and hands them back, without looking inside.

Ruby implementation

memento.rb
# 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
end

In 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.define makes 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.freeze on save, and .dup on restore, so neither the snapshot nor the restored game can change the other.
  • private_constant :Snapshot stops 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.