Memento Pattern #
A user is filling out a long multi-step registration form — personal data, company data, product preferences, document uploads. On the fourth step, they realize something needs changing back on the first step. Without a snapshot mechanism, the only option is to go back to the start and fill everything in again. With the Memento Pattern, every move to the next step stores a snapshot of the current state; going back to any step means restoring that snapshot, not starting over. The code that saves and restores snapshots does not need to know what the state contains or how the form represents it — encapsulation is preserved because a Memento object can only be used by the object that created it.
What Is the Memento Pattern? #
The Memento Pattern is a behavioral design pattern that lets you save and restore an object’s previous state without exposing its internal implementation details. The object whose state you want to save creates a “snapshot” in the form of a Memento object; this snapshot can be stored by a Caretaker and handed back to the originator at any time to restore the previous state.
Three properties define the Memento Pattern:
- Encapsulation preserved — the Caretaker that stores a Memento cannot read or modify the state contents; only the Originator can create it and restore from it
- Complete snapshot — the Memento stores everything needed to return the object to exactly the condition it was in when the snapshot was taken
- Separation of responsibilities — the Originator knows what needs to be stored; the Caretaker knows when and how many snapshots to keep
sequenceDiagram
participant C as Caretaker
participant O as Originator (Form)
participant M as Memento (Snapshot)
C->>O: Request snapshot
O->>M: CreateMemento() — create a state snapshot
O-->>C: return Memento
C->>C: store Memento in stack
Note over O: User changes state
C->>O: Restore(memento)
O->>M: Read state from Memento
M-->>O: previous state
Note over O: State restored to the snapshotDifference from the Command Pattern for Undo #
Memento and the Command Pattern both support undo, but in different ways. Choosing the right one depends on the characteristics of the state being undone.
// Command Pattern for Undo:
// Store the ACTION performed + how to reverse it
type InsertCommand struct {
editor *Editor
char rune
position int
}
func (c *InsertCommand) Undo() error {
// Reverse the action: delete the character just inserted
_, err := c.editor.Delete(c.position)
return err
}
// Good for: discrete actions that are easy to reverse logically
// Memento Pattern for Undo:
// Store a STATE SNAPSHOT before the action
type EditorMemento struct {
content string
cursor int
}
// Undo = restore the snapshot
// Good for: complex state that is hard to reverse action by action
| Aspect | Command Pattern | Memento Pattern |
|---|---|---|
| What is stored | The action + how to reverse it | A state snapshot |
| Best for | Discrete, easily inverted actions | Complex state that is hard to invert |
| Memory | More economical — only deltas | More expensive — the entire state |
| Partial undo | Can undo part of an action | Must restore the whole snapshot |
| Examples | Text insert/delete | Form wizard, game saves |
Three Components of the Memento Pattern #
classDiagram
class FormWizard {
-currentStep int
-personalData PersonalData
-companyData CompanyData
-preferences Preferences
+CreateMemento() WizardMemento
+RestoreFromMemento(m WizardMemento)
+NextStep()
+PrevStep()
}
class WizardMemento {
-step int
-personalData PersonalData
-companyData CompanyData
-preferences Preferences
-createdAt time.Time
+CreatedAt() time.Time
+StepNumber() int
}
class WizardHistory {
-snapshots []WizardMemento
-maxSize int
+Save(originator FormWizard)
+Undo(originator FormWizard)
+Count() int
}
FormWizard ..> WizardMemento : creates
FormWizard <.. WizardMemento : restores state
WizardHistory o-- WizardMemento : manages
WizardHistory --> FormWizard : interacts with| Component | Role | Who May Access |
|---|---|---|
| Originator | Creates and restores Mementos | All Memento fields (full access) |
| Memento | Stores a snapshot of the Originator’s state | Originator only — fields are unexported |
| Caretaker | Stores and returns Mementos | Only the Memento interface — cannot read the state |
Full Implementation: Multi-Step Form Wizard #
Originator: FormWizard #
package wizard
import (
"fmt"
"time"
)
// PersonalData stores the user's personal data.
type PersonalData struct {
FullName string
Email string
PhoneNumber string
BirthDate string
}
// CompanyData stores the company data.
type CompanyData struct {
CompanyName string
Industry string
Size string
Website string
}
// Preferences stores the user's product preferences.
type Preferences struct {
ProductTier string
BillingCycle string
Addons []string
NewsletterOpt bool
}
// WizardMemento stores a snapshot of the wizard state at one point in time.
// These fields are unexported — only FormWizard can read and write them.
type WizardMemento struct {
step int
personal PersonalData
company CompanyData
preferences Preferences
createdAt time.Time
}
// CreatedAt returns when the snapshot was created — the only method the Caretaker may access.
func (m *WizardMemento) CreatedAt() time.Time { return m.createdAt }
// StepNumber returns the step at which the snapshot was created.
func (m *WizardMemento) StepNumber() int { return m.step }
// FormWizard is the Originator — the object whose state we want to save.
type FormWizard struct {
currentStep int
personal PersonalData
company CompanyData
preferences Preferences
maxSteps int
}
func NewFormWizard() *FormWizard {
return &FormWizard{
currentStep: 1,
maxSteps: 4,
}
}
// CreateMemento creates a snapshot of the wizard's current state.
// A deep copy is made to ensure the Memento is independent of the wizard's state.
func (w *FormWizard) CreateMemento() *WizardMemento {
// Deep copy Preferences.Addons (slice — needs copying)
addonsCopy := make([]string, len(w.preferences.Addons))
copy(addonsCopy, w.preferences.Addons)
return &WizardMemento{
step: w.currentStep,
personal: PersonalData{
FullName: w.personal.FullName,
Email: w.personal.Email,
PhoneNumber: w.personal.PhoneNumber,
BirthDate: w.personal.BirthDate,
},
company: CompanyData{
CompanyName: w.company.CompanyName,
Industry: w.company.Industry,
Size: w.company.Size,
Website: w.company.Website,
},
preferences: Preferences{
ProductTier: w.preferences.ProductTier,
BillingCycle: w.preferences.BillingCycle,
Addons: addonsCopy,
NewsletterOpt: w.preferences.NewsletterOpt,
},
createdAt: time.Now(),
}
}
// RestoreFromMemento restores the wizard state from a Memento.
func (w *FormWizard) RestoreFromMemento(m *WizardMemento) {
// Deep copy again to avoid sharing the slice
addonsCopy := make([]string, len(m.preferences.Addons))
copy(addonsCopy, m.preferences.Addons)
w.currentStep = m.step
w.personal = m.personal
w.company = m.company
w.preferences = m.preferences
w.preferences.Addons = addonsCopy
}
// Step 1: Personal Data
func (w *FormWizard) SetPersonalData(data PersonalData) error {
if data.FullName == "" || data.Email == "" {
return fmt.Errorf("full name and email are required")
}
w.personal = data
return nil
}
// Step 2: Company Data
func (w *FormWizard) SetCompanyData(data CompanyData) error {
if data.CompanyName == "" {
return fmt.Errorf("company name is required")
}
w.company = data
return nil
}
// Step 3: Preferences
func (w *FormWizard) SetPreferences(prefs Preferences) error {
if prefs.ProductTier == "" {
return fmt.Errorf("product tier must be selected")
}
w.preferences = prefs
return nil
}
// NextStep moves to the next step.
func (w *FormWizard) NextStep() error {
if w.currentStep >= w.maxSteps {
return fmt.Errorf("already at final step")
}
w.currentStep++
return nil
}
// PrevStep moves back to the previous step.
func (w *FormWizard) PrevStep() error {
if w.currentStep <= 1 {
return fmt.Errorf("already at first step")
}
w.currentStep--
return nil
}
// CurrentStep returns the current step.
func (w *FormWizard) CurrentStep() int { return w.currentStep }
// Summary returns a summary of the wizard's current data.
func (w *FormWizard) Summary() string {
return fmt.Sprintf("Step %d/%d | %s (%s) | %s | %s",
w.currentStep, w.maxSteps,
w.personal.FullName, w.personal.Email,
w.company.CompanyName,
w.preferences.ProductTier,
)
}
Caretaker: WizardHistory #
package wizard
import (
"fmt"
"time"
)
// SnapshotInfo stores snapshot metadata for display to the user.
type SnapshotInfo struct {
Index int
Step int
CreatedAt time.Time
}
// WizardHistory is the Caretaker — it stores and manages Mementos.
// It cannot read the state inside a Memento — only store and return it.
type WizardHistory struct {
snapshots []*WizardMemento
maxSize int
}
func NewWizardHistory(maxSize int) *WizardHistory {
return &WizardHistory{
snapshots: make([]*WizardMemento, 0, maxSize),
maxSize: maxSize,
}
}
// Save stores a snapshot of the wizard's current state.
func (h *WizardHistory) Save(wizard *FormWizard) {
if len(h.snapshots) >= h.maxSize {
// Remove the oldest snapshot to make room
h.snapshots = h.snapshots[1:]
}
h.snapshots = append(h.snapshots, wizard.CreateMemento())
fmt.Printf("[History] Snapshot saved for step %d (total: %d)\n",
wizard.CurrentStep(), len(h.snapshots))
}
// Undo restores the latest snapshot to the wizard.
func (h *WizardHistory) Undo(wizard *FormWizard) error {
if len(h.snapshots) == 0 {
return fmt.Errorf("no snapshot available to restore")
}
// Pop the latest snapshot
last := h.snapshots[len(h.snapshots)-1]
h.snapshots = h.snapshots[:len(h.snapshots)-1]
wizard.RestoreFromMemento(last)
fmt.Printf("[History] State restored to step %d (remaining snapshots: %d)\n",
last.StepNumber(), len(h.snapshots))
return nil
}
// GoToStep restores the snapshot for a specific step.
func (h *WizardHistory) GoToStep(wizard *FormWizard, targetStep int) error {
for i := len(h.snapshots) - 1; i >= 0; i-- {
if h.snapshots[i].StepNumber() == targetStep {
wizard.RestoreFromMemento(h.snapshots[i])
// Remove all snapshots after this one
h.snapshots = h.snapshots[:i+1]
return nil
}
}
return fmt.Errorf("no snapshot for step %d", targetStep)
}
// ListSnapshots returns the list of stored snapshots for display to the user.
// The Caretaker cannot access the state inside Mementos — only metadata.
func (h *WizardHistory) ListSnapshots() []SnapshotInfo {
info := make([]SnapshotInfo, len(h.snapshots))
for i, snap := range h.snapshots {
info[i] = SnapshotInfo{
Index: i,
Step: snap.StepNumber(),
CreatedAt: snap.CreatedAt(),
}
}
return info
}
// Count returns the number of stored snapshots.
func (h *WizardHistory) Count() int { return len(h.snapshots) }
Demonstration #
func main() {
wizard := wizard.NewFormWizard()
history := wizard.NewWizardHistory(10)
// Step 1: Fill in personal data and save a snapshot
_ = wizard.SetPersonalData(wizard.PersonalData{
FullName: "Budi Santoso",
Email: "[email protected]",
PhoneNumber: "+6281234567890",
})
history.Save(wizard)
_ = wizard.NextStep()
fmt.Printf("After step 1: %s\n", wizard.Summary())
// Step 2: Fill in company data and save a snapshot
_ = wizard.SetCompanyData(wizard.CompanyData{
CompanyName: "PT Maju Bersama",
Industry: "Technology",
Size: "51-200",
})
history.Save(wizard)
_ = wizard.NextStep()
fmt.Printf("After step 2: %s\n", wizard.Summary())
// Step 3: Fill in preferences
_ = wizard.SetPreferences(wizard.Preferences{
ProductTier: "Enterprise",
BillingCycle: "annual",
Addons: []string{"sso", "audit-log"},
})
history.Save(wizard)
_ = wizard.NextStep()
fmt.Printf("After step 3: %s\n", wizard.Summary())
// The user wants to go back to step 1 to correct the email
fmt.Println("\n--- User wants to go back to step 1 ---")
_ = history.GoToStep(wizard, 1)
fmt.Printf("After going back to step 1: %s\n", wizard.Summary())
// Correct the email
_ = wizard.SetPersonalData(wizard.PersonalData{
FullName: "Budi Santoso",
Email: "[email protected]", // new email
PhoneNumber: "+6281234567890",
})
fmt.Printf("After email correction: %s\n", wizard.Summary())
// Undo one step
fmt.Println("\n--- Undo ---")
_ = history.Undo(wizard)
fmt.Printf("After undo: %s\n", wizard.Summary())
}
Second Case Study: Game Checkpoint System #
Games are the most classic Memento use case — every checkpoint stores the entire game state (player position, health, inventory, level progress) so the player can return to a checkpoint when they die.
package game
import (
"fmt"
"time"
)
// PlayerState stores the player's complete state for snapshots.
type PlayerState struct {
Level int
Health int
MaxHealth int
Position Position
Inventory []Item
Gold int
Skills map[string]int
QuestLog []Quest
}
// Position represents the player's position in the game world.
type Position struct {
X, Y, Z float64
Map string
}
// Item represents one item in the inventory.
type Item struct {
ID string
Name string
Quantity int
Equipped bool
}
// Quest represents a quest in progress or completed.
type Quest struct {
ID string
Name string
Progress int
Complete bool
}
// GameMemento stores a snapshot of the game state.
type GameMemento struct {
state PlayerState
savedAt time.Time
checkpointName string
}
func (m *GameMemento) SavedAt() time.Time { return m.savedAt }
func (m *GameMemento) CheckpointName() string { return m.checkpointName }
// Player is the Originator in the game context.
type Player struct {
state PlayerState
}
func NewPlayer() *Player {
return &Player{
state: PlayerState{
Level: 1,
Health: 100,
MaxHealth: 100,
Position: Position{X: 0, Y: 0, Z: 0, Map: "starting_village"},
Inventory: make([]Item, 0),
Gold: 50,
Skills: map[string]int{"sword": 1, "shield": 1},
QuestLog: make([]Quest, 0),
},
}
}
// Save creates a snapshot of the player's current state.
func (p *Player) Save(checkpointName string) *GameMemento {
// Deep copy all reference types
inventoryCopy := make([]Item, len(p.state.Inventory))
copy(inventoryCopy, p.state.Inventory)
skillsCopy := make(map[string]int, len(p.state.Skills))
for k, v := range p.state.Skills {
skillsCopy[k] = v
}
questsCopy := make([]Quest, len(p.state.QuestLog))
copy(questsCopy, p.state.QuestLog)
return &GameMemento{
state: PlayerState{
Level: p.state.Level,
Health: p.state.Health,
MaxHealth: p.state.MaxHealth,
Position: p.state.Position, // struct — value copy is safe
Inventory: inventoryCopy,
Gold: p.state.Gold,
Skills: skillsCopy,
QuestLog: questsCopy,
},
savedAt: time.Now(),
checkpointName: checkpointName,
}
}
// Restore restores the player state from a Memento.
func (p *Player) Restore(m *GameMemento) {
inventoryCopy := make([]Item, len(m.state.Inventory))
copy(inventoryCopy, m.state.Inventory)
skillsCopy := make(map[string]int, len(m.state.Skills))
for k, v := range m.state.Skills {
skillsCopy[k] = v
}
questsCopy := make([]Quest, len(m.state.QuestLog))
copy(questsCopy, m.state.QuestLog)
p.state = PlayerState{
Level: m.state.Level,
Health: m.state.Health,
MaxHealth: m.state.MaxHealth,
Position: m.state.Position,
Inventory: inventoryCopy,
Gold: m.state.Gold,
Skills: skillsCopy,
QuestLog: questsCopy,
}
fmt.Printf("[Game] Restored to checkpoint '%s' at %s\n",
m.checkpointName, m.savedAt.Format("15:04:05"))
}
// Player actions
func (p *Player) TakeDamage(amount int) {
p.state.Health -= amount
if p.state.Health < 0 {
p.state.Health = 0
}
}
func (p *Player) AddItem(item Item) {
p.state.Inventory = append(p.state.Inventory, item)
}
func (p *Player) SpendGold(amount int) error {
if p.state.Gold < amount {
return fmt.Errorf("not enough gold")
}
p.state.Gold -= amount
return nil
}
func (p *Player) MoveTo(pos Position) {
p.state.Position = pos
}
func (p *Player) LevelUp() {
p.state.Level++
p.state.MaxHealth += 20
p.state.Health = p.state.MaxHealth
}
func (p *Player) StatusReport() string {
return fmt.Sprintf("Level %d | HP %d/%d | Gold %d | Map: %s (%.0f,%.0f)",
p.state.Level, p.state.Health, p.state.MaxHealth,
p.state.Gold, p.state.Position.Map,
p.state.Position.X, p.state.Position.Y,
)
}
func (p *Player) IsDead() bool { return p.state.Health <= 0 }
// SaveGameManager is the Caretaker for the game — it manages save slots.
type SaveGameManager struct {
slots map[string]*GameMemento // slot name → memento
maxSlots int
}
func NewSaveGameManager(maxSlots int) *SaveGameManager {
return &SaveGameManager{
slots: make(map[string]*GameMemento),
maxSlots: maxSlots,
}
}
// SaveToSlot stores a snapshot into a specific slot.
func (m *SaveGameManager) SaveToSlot(player *Player, slotName string) {
memento := player.Save(slotName)
m.slots[slotName] = memento
fmt.Printf("[SaveManager] Game saved to slot '%s'\n", slotName)
}
// LoadFromSlot restores the state from a specific slot.
func (m *SaveGameManager) LoadFromSlot(player *Player, slotName string) error {
memento, ok := m.slots[slotName]
if !ok {
return fmt.Errorf("save slot '%s' not found", slotName)
}
player.Restore(memento)
return nil
}
// ListSlots returns the list of available save slots.
func (m *SaveGameManager) ListSlots() []string {
slots := make([]string, 0, len(m.slots))
for name := range m.slots {
slots = append(slots, name)
}
return slots
}
// Demonstration
func playGame() {
player := NewPlayer()
saveManager := NewSaveGameManager(5)
fmt.Printf("Start: %s\n", player.StatusReport())
// Checkpoint 1: In the starting village
saveManager.SaveToSlot(player, "checkpoint_village")
// Player progresses
player.LevelUp()
player.AddItem(Item{ID: "iron_sword", Name: "Iron Sword", Quantity: 1, Equipped: true})
player.MoveTo(Position{X: 150, Y: 0, Z: 0, Map: "dark_forest"})
fmt.Printf("After exploring: %s\n", player.StatusReport())
// Checkpoint 2: Before the boss fight
saveManager.SaveToSlot(player, "checkpoint_before_boss")
// Player loses the boss fight
player.TakeDamage(200)
fmt.Printf("After boss fight: %s\n", player.StatusReport())
if player.IsDead() {
fmt.Println("\n--- Player died! Loading checkpoint... ---")
_ = saveManager.LoadFromSlot(player, "checkpoint_before_boss")
fmt.Printf("After loading: %s\n", player.StatusReport())
}
}
Serializing Mementos to Persistent Storage #
For game saves that survive the application closing, Mementos need to be serialized to disk or a database.
package game
import (
"encoding/json"
"os"
"path/filepath"
"fmt"
)
// PersistentSaveManager stores Mementos on the file system.
type PersistentSaveManager struct {
saveDir string
}
func NewPersistentSaveManager(saveDir string) (*PersistentSaveManager, error) {
if err := os.MkdirAll(saveDir, 0755); err != nil {
return nil, fmt.Errorf("cannot create save directory: %w", err)
}
return &PersistentSaveManager{saveDir: saveDir}, nil
}
// serializedMemento is a JSON-serializable version of GameMemento.
// This is needed because GameMemento's fields are unexported.
type serializedMemento struct {
State PlayerState `json:"state"`
SavedAt string `json:"saved_at"`
CheckpointName string `json:"checkpoint_name"`
}
// SaveToDisk stores a Memento to a file.
func (m *PersistentSaveManager) SaveToDisk(player *Player, slotName string) error {
memento := player.Save(slotName)
// Serialization — we need access to the internal fields
// In a real implementation, the Originator provides an ExportForSave() method
serialized := serializedMemento{
State: memento.state,
SavedAt: memento.savedAt.Format(time.RFC3339),
CheckpointName: memento.checkpointName,
}
data, err := json.MarshalIndent(serialized, "", " ")
if err != nil {
return fmt.Errorf("cannot serialize save: %w", err)
}
filename := filepath.Join(m.saveDir, slotName+".json")
if err := os.WriteFile(filename, data, 0644); err != nil {
return fmt.Errorf("cannot write save file: %w", err)
}
fmt.Printf("[PersistentSave] Saved to %s\n", filename)
return nil
}
// LoadFromDisk loads a Memento from a file and restores it to the player.
func (m *PersistentSaveManager) LoadFromDisk(player *Player, slotName string) error {
filename := filepath.Join(m.saveDir, slotName+".json")
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("save file not found: %w", err)
}
var serialized serializedMemento
if err := json.Unmarshal(data, &serialized); err != nil {
return fmt.Errorf("cannot deserialize save: %w", err)
}
// Reconstruct the Memento from the deserialized data
savedAt, _ := time.Parse(time.RFC3339, serialized.SavedAt)
memento := &GameMemento{
state: serialized.State,
savedAt: savedAt,
checkpointName: serialized.CheckpointName,
}
player.Restore(memento)
fmt.Printf("[PersistentSave] Loaded from %s\n", filename)
return nil
}
Memory Management: Limiting Snapshot Count #
Storing too many snapshots can exhaust memory, especially for large states. There are several strategies to deal with this.
// Strategy 1: Fixed-size ring buffer — drop the oldest when full
type BoundedHistory struct {
snapshots []*WizardMemento
maxSize int
writeIdx int
count int
}
func NewBoundedHistory(maxSize int) *BoundedHistory {
return &BoundedHistory{
snapshots: make([]*WizardMemento, maxSize),
maxSize: maxSize,
}
}
func (h *BoundedHistory) Save(wizard *FormWizard) {
h.snapshots[h.writeIdx] = wizard.CreateMemento()
h.writeIdx = (h.writeIdx + 1) % h.maxSize
if h.count < h.maxSize {
h.count++
}
}
// Strategy 2: Snapshots with delta compression
// Store a full state only every N steps; store only deltas in between
type DeltaHistory struct {
baseSnapshot *WizardMemento
deltas []WizardDelta
fullSnapshotN int // store a full snapshot every N changes
}
type WizardDelta struct {
changedFields map[string]interface{}
timestamp time.Time
}
// Strategy 3: Expired snapshots — remove snapshots that are too old
type TimedHistory struct {
snapshots []*timedMemento
maxAge time.Duration
}
type timedMemento struct {
memento *WizardMemento
expiresAt time.Time
}
func (h *TimedHistory) PurgeExpired() {
now := time.Now()
var active []*timedMemento
for _, tm := range h.snapshots {
if now.Before(tm.expiresAt) {
active = append(active, tm)
}
}
h.snapshots = active
}
Large State = Expensive Snapshots
If the snapshotted object has large slices, maps with many entries, or deeply nested structs, every snapshot can consume significant memory. For such cases, consider: (1) snapshotting only the fields that changed (delta snapshots), (2) limiting the maximum snapshot count with a ring buffer, (3) compressing snapshots before storing, or (4) serializing directly to disk for very large states.
Testing the Memento Pattern #
func TestFormWizard_CreateAndRestoreMemento(t *testing.T) {
wizard := NewFormWizard()
history := NewWizardHistory(5)
// Set the initial state
_ = wizard.SetPersonalData(PersonalData{FullName: "Alice", Email: "[email protected]"})
history.Save(wizard)
_ = wizard.NextStep()
// Change the state
_ = wizard.SetCompanyData(CompanyData{CompanyName: "Test Corp"})
_ = wizard.NextStep()
// Verify the state changed
if wizard.CurrentStep() != 3 {
t.Errorf("expected step 3, got %d", wizard.CurrentStep())
}
// Undo to step 1
_ = history.Undo(wizard)
if wizard.CurrentStep() != 1 {
t.Errorf("after undo: expected step 1, got %d", wizard.CurrentStep())
}
if wizard.personal.FullName != "Alice" {
t.Errorf("personal data should be restored, got %q", wizard.personal.FullName)
}
}
func TestWizardHistory_BoundedByMaxSize(t *testing.T) {
wizard := NewFormWizard()
history := NewWizardHistory(3) // maximum 3 snapshots
for i := 0; i < 5; i++ {
_ = wizard.SetPersonalData(PersonalData{
FullName: fmt.Sprintf("User %d", i),
Email: fmt.Sprintf("user%[email protected]", i),
})
history.Save(wizard)
}
if history.Count() > 3 {
t.Errorf("expected max 3 snapshots, got %d", history.Count())
}
}
func TestWizardHistory_GoToStep(t *testing.T) {
wizard := NewFormWizard()
history := NewWizardHistory(10)
for step := 1; step <= 3; step++ {
history.Save(wizard)
_ = wizard.NextStep()
}
err := history.GoToStep(wizard, 2)
if err != nil {
t.Fatalf("GoToStep failed: %v", err)
}
if wizard.CurrentStep() != 2 {
t.Errorf("expected step 2 after GoToStep, got %d", wizard.CurrentStep())
}
}
func TestGameMemento_DeepCopyInventory(t *testing.T) {
player := NewPlayer()
player.AddItem(Item{ID: "sword", Name: "Iron Sword", Quantity: 1})
// Create a snapshot
memento := player.Save("test")
// Modify the inventory after the snapshot
player.state.Inventory[0].Quantity = 99
// Restore — the inventory must return to quantity 1, not 99
player.Restore(memento)
if player.state.Inventory[0].Quantity != 1 {
t.Errorf("expected quantity 1 after restore, got %d (shallow copy bug!)",
player.state.Inventory[0].Quantity)
}
}
func TestGameMemento_UndoAfterDeath(t *testing.T) {
player := NewPlayer()
saveManager := NewSaveGameManager(3)
initialHealth := player.state.Health
saveManager.SaveToSlot(player, "before_boss")
player.TakeDamage(200)
if !player.IsDead() {
t.Error("player should be dead after 200 damage")
}
err := saveManager.LoadFromSlot(player, "before_boss")
if err != nil {
t.Fatalf("LoadFromSlot failed: %v", err)
}
if player.state.Health != initialHealth {
t.Errorf("health should be restored to %d, got %d", initialHealth, player.state.Health)
}
if player.IsDead() {
t.Error("player should not be dead after restore")
}
}
When to Use and When Not to #
USE Memento if:
✓ You need to store snapshots of complex state for undo/redo or rollback
✓ The object's state is too complex to reverse logically (the Command Pattern is not enough)
✓ Encapsulation must be preserved — the Caretaker must not see the state contents
✓ You need multiple navigable save points (form wizard, game checkpoints)
✓ State needs to be stored to persistent storage for later restore
AVOID Memento if:
✗ The object's state is very large — every snapshot consumes significant memory
✗ The state changes very frequently — too many snapshots need to be created
✗ The actions needing undo are discrete and easy to reverse — use Command
✗ You only need one-step undo — storing a single copy of the previous state is enough
Memento Review Checklist #
DESIGN:
□ Memento fields are unexported — only the Originator can read/write the state
□ The Caretaker only accesses Memento metadata (CreatedAt, Name), not the state
□ Mementos are immutable after creation — no setters
□ The Originator does deep copies in CreateMemento and RestoreFromMemento
DEEP COPY:
□ All slices are copied (not shared references)
□ All maps are copied (not shared references)
□ All nested pointer structs are copied explicitly
□ No shared mutable state between the Originator and the Memento
MEMORY:
□ There is a maximum snapshot count (ring buffer or eviction policy)
□ Snapshots no longer needed are removed from the Caretaker
□ Large states consider delta snapshots or compression
TESTING:
□ State after Restore is exactly the same as the state at CreateMemento
□ Modifying state after CreateMemento does not affect the Memento (deep copy)
□ Modifying the Memento does not affect the Originator's state (deep copy in Restore)
□ The maximum snapshot limit is verified
Summary #
- Memento stores state snapshots without breaking encapsulation — the Caretaker stores Mementos but cannot read their contents; only the Originator can create and restore Mementos.
- Three components: Originator (creates and restores Mementos), Memento (stores state with unexported fields), and Caretaker (stores and returns Mementos without knowing their contents).
- Deep copy is a duty — every slice, map, and pointer in the state must be copied deeply during CreateMemento and RestoreFromMemento; a shallow copy creates shared mutable state that becomes a source of bugs.
- Different from Command: Command stores the action and how to reverse it; Memento stores a complete state snapshot — choose based on the complexity of the state being undone.
- Limit the snapshot count — use a ring buffer or eviction policy to prevent memory leaks from unlimited snapshot accumulation.
- Serialization to disk lets Mementos survive after the application closes — useful for game saves, form drafts, and session recovery.
- Form wizards and game checkpoints are the two most natural use cases — both need the ability to return to a specific point without starting over.
- For very large states, consider delta snapshots (store only what changed) or compression before storing.