Memento Pattern: Capturing and Restoring Object State

Learning Objectives: By the end of this lesson, you will be able to:

  • Understand how to capture and restore object state without violating encapsulation
  • Implement undo/redo functionality using memento snapshots
  • Design efficient state management systems with minimal memory overhead
  • Apply the pattern to text editors, game save systems, and transactional operations
  • Evaluate trade-offs between snapshot frequency, memory usage, and restoration granularity

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Have you ever accidentally deleted important work in a text editor and felt immense relief when you pressed Ctrl+Z to undo the action? Or saved your progress in a video game before attempting a challenging boss fight, knowing you could reload if things went wrong? These everyday experiences demonstrate the Memento pattern in action - the ability to capture a snapshot of state at a specific point in time and restore it later without exposing internal implementation details.

Lesson Overview

  • Duration: 3 hours
  • Prerequisites: Understanding of object-oriented programming, encapsulation principles, and basic data structures
  • Materials Needed: IDE with Java/C#/Python support, memory profiling tools (optional)

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Memento Fundamentals - Understanding state capture and restoration without breaking encapsulation
  2. Implementation Patterns - Different approaches to memento storage and management
  3. Memory Optimization - Strategies for efficient state management and incremental snapshots
  4. Real-world Applications - Undo systems, version control, game saves, and transaction management
  5. Performance Considerations - Balancing memory usage, restoration speed, and snapshot frequency

Success Criteria:

By the end of this session, you should be able to design robust state management systems that preserve encapsulation while providing flexible undo/redo capabilities and efficient state restoration.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

State Capture Without Violating Encapsulation

The Memento pattern provides a way to capture an object’s internal state so that it can be restored later, all without violating encapsulation. The key insight is that the object itself creates a representation of its state (a memento) that can be stored externally but only the originating object can interpret and restore from.

Real-World Analogy: Think of a medical patient’s chart. The chart contains a snapshot of the patient’s condition at a specific time, but only qualified medical professionals can interpret and act on that information. The chart doesn’t expose the patient’s private details to everyone - it’s a controlled representation that preserves privacy while enabling appropriate access.

Why This Matters: Traditional approaches to saving object state often require making internal fields public or providing extensive getter methods, which breaks encapsulation. The Memento pattern maintains object integrity while still enabling powerful state management features.

Three Key Participants

The pattern involves three main roles:

  1. Originator: The object whose state needs to be saved and restored
  2. Memento: The snapshot object that stores the originator’s state
  3. Caretaker: The object that manages mementos but doesn’t examine their contents

Key Terminology

  • Snapshot: A point-in-time capture of an object’s complete state
  • Encapsulation Boundary: The protection of internal object details from external access
  • State Restoration: The process of returning an object to a previously captured state
  • Caretaker: The manager of memento storage and retrieval
  • Incremental Snapshot: A memento that stores only changes since the last snapshot

4. Detailed Explanation and Examples (25-30 minutes)

Understanding the Memento Pattern

The Memento pattern is a behavioral design pattern that captures and externalizes an object’s internal state so that the object can be restored to this state later, all without violating encapsulation. The pattern is particularly useful for implementing undo mechanisms, checkpointing systems, and any scenario where you need to revert objects to previous states.

The pattern works by having the originator create a memento object that contains a snapshot of its current state. This memento is then stored by a caretaker, which doesn’t examine or modify the memento’s contents. When restoration is needed, the caretaker provides the memento back to the originator, which uses it to restore its state.

Practical Applications

Text Editors and IDEs: Every time you press Ctrl+Z in your favorite text editor, you’re experiencing the Memento pattern. The editor captures snapshots of document state at various points, allowing you to undo changes without exposing the internal document structure.

Game Development: Video games use memento patterns for save systems, allowing players to save their progress and return to specific points later. This includes not just player position, but entire game world state, inventory, and progress flags.

Database Transactions: Database management systems use memento-like patterns for transaction rollback, capturing database state before transactions begin and restoring it if transactions fail.

Version Control Systems: Tools like Git use sophisticated memento patterns to track file and project state changes over time, enabling branching, merging, and rollback operations.

Common Misconceptions

  1. Misconception: Memento is just serialization Reality: While serialization can be used to implement mementos, the pattern is about the relationship between originator, memento, and caretaker, not the storage mechanism

  2. Misconception: All object data should be stored in every memento Reality: Effective memento implementations often use incremental or differential storage to minimize memory usage

  3. Misconception: Mementos should be readable by external classes Reality: The whole point is that only the originator can interpret the memento’s contents, preserving encapsulation

5. Live Coding Demonstration (20-25 minutes)

Example 1: Text Editor with Undo/Redo Functionality

import java.util.*;

// Memento class - stores state but doesn't expose it externally
class TextMemento {
    private final String content;
    private final int cursorPosition;
    private final long timestamp;
    
    // Package-private constructor - only originator can create
    TextMemento(String content, int cursorPosition) {
        this.content = content;
        this.cursorPosition = cursorPosition;
        this.timestamp = System.currentTimeMillis();
    }
    
    // Package-private getters - only originator can access
    String getContent() {
        return content;
    }
    
    int getCursorPosition() {
        return cursorPosition;
    }
    
    long getTimestamp() {
        return timestamp;
    }
    
    // Public method for caretaker information (doesn't expose state details)
    public String getInfo() {
        return "Snapshot at " + new Date(timestamp) + " (" + content.length() + " chars)";
    }
}

// Originator class - creates and restores from mementos
class TextEditor {
    private StringBuilder content;
    private int cursorPosition;
    
    public TextEditor() {
        this.content = new StringBuilder();
        this.cursorPosition = 0;
    }
    
    // Core functionality
    public void insertText(String text) {
        content.insert(cursorPosition, text);
        cursorPosition += text.length();
        System.out.println("Inserted: '" + text + "' at position " + 
                          (cursorPosition - text.length()));
    }
    
    public void deleteText(int length) {
        if (cursorPosition >= length) {
            String deleted = content.substring(cursorPosition - length, cursorPosition);
            content.delete(cursorPosition - length, cursorPosition);
            cursorPosition -= length;
            System.out.println("Deleted: '" + deleted + "'");
        }
    }
    
    public void setCursorPosition(int position) {
        if (position >= 0 && position <= content.length()) {
            this.cursorPosition = position;
            System.out.println("Cursor moved to position " + position);
        }
    }
    
    // Memento creation - captures current state
    public TextMemento createMemento() {
        System.out.println("Creating memento...");
        return new TextMemento(content.toString(), cursorPosition);
    }
    
    // State restoration - restores from memento
    public void restoreFromMemento(TextMemento memento) {
        if (memento != null) {
            this.content = new StringBuilder(memento.getContent());
            this.cursorPosition = memento.getCursorPosition();
            System.out.println("Restored from memento: " + memento.getInfo());
        }
    }
    
    // Display current state
    public void displayState() {
        System.out.println("Content: '" + content.toString() + "'");
        System.out.println("Cursor at position: " + cursorPosition);
        System.out.println("Length: " + content.length());
    }
}

Explanation:

  • TextMemento stores the editor’s state but only provides access to the originator (TextEditor)
  • The memento’s constructor and state accessors are package-private, maintaining encapsulation
  • The originator (TextEditor) is the only class that can create and interpret mementos

Example 2: Caretaker with Undo/Redo Stack Management

// Caretaker class - manages mementos without examining their contents
class EditorHistory {
    private final Stack<TextMemento> undoStack;
    private final Stack<TextMemento> redoStack;
    private final int maxHistorySize;
    
    public EditorHistory(int maxHistorySize) {
        this.undoStack = new Stack<>();
        this.redoStack = new Stack<>();
        this.maxHistorySize = maxHistorySize;
    }
    
    public void saveState(TextMemento memento) {
        // Clear redo stack when new state is saved
        redoStack.clear();
        
        // Add to undo stack
        undoStack.push(memento);
        
        // Limit history size to prevent memory issues
        if (undoStack.size() > maxHistorySize) {
            undoStack.remove(0); // Remove oldest memento
        }
        
        System.out.println("State saved. Undo stack size: " + undoStack.size());
    }
    
    public TextMemento undo() {
        if (!undoStack.isEmpty()) {
            TextMemento memento = undoStack.pop();
            redoStack.push(memento);
            System.out.println("Undo performed. Available undos: " + undoStack.size());
            return memento;
        } else {
            System.out.println("Nothing to undo");
            return null;
        }
    }
    
    public TextMemento redo() {
        if (!redoStack.isEmpty()) {
            TextMemento memento = redoStack.pop();
            undoStack.push(memento);
            System.out.println("Redo performed. Available redos: " + redoStack.size());
            return memento;
        } else {
            System.out.println("Nothing to redo");
            return null;
        }
    }
    
    public boolean canUndo() {
        return !undoStack.isEmpty();
    }
    
    public boolean canRedo() {
        return !redoStack.isEmpty();
    }
    
    public void showHistory() {
        System.out.println("History Summary:");
        System.out.println("  Undo available: " + undoStack.size() + " states");
        System.out.println("  Redo available: " + redoStack.size() + " states");
        
        if (!undoStack.isEmpty()) {
            System.out.println("  Recent states:");
            for (int i = Math.max(0, undoStack.size() - 3); i < undoStack.size(); i++) {
                System.out.println("    " + (i + 1) + ". " + undoStack.get(i).getInfo());
            }
        }
    }
    
    public void clearHistory() {
        undoStack.clear();
        redoStack.clear();
        System.out.println("History cleared");
    }
}

Key Points:

  • The caretaker manages memento storage and retrieval without examining contents
  • Undo/redo stacks provide intuitive navigation through state history
  • Memory management prevents unlimited history growth

Example 3: Complete Text Editor Application

// Complete application demonstrating the pattern
public class TextEditorApp {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        EditorHistory history = new EditorHistory(10);
        
        // Demonstrate text editing with undo/redo
        System.out.println("=== Starting Text Editor Demo ===");
        
        // Initial state
        history.saveState(editor.createMemento());
        editor.displayState();
        
        // Edit 1: Insert text
        System.out.println("\n--- Edit 1: Insert 'Hello' ---");
        editor.insertText("Hello");
        history.saveState(editor.createMemento());
        editor.displayState();
        
        // Edit 2: Insert more text
        System.out.println("\n--- Edit 2: Insert ' World' ---");
        editor.insertText(" World");
        history.saveState(editor.createMemento());
        editor.displayState();
        
        // Edit 3: Move cursor and insert
        System.out.println("\n--- Edit 3: Move cursor and insert ---");
        editor.setCursorPosition(5);
        editor.insertText(" Beautiful");
        history.saveState(editor.createMemento());
        editor.displayState();
        
        // Demonstrate undo
        System.out.println("\n--- Undo Operations ---");
        if (history.canUndo()) {
            editor.restoreFromMemento(history.undo());
            editor.displayState();
        }
        
        if (history.canUndo()) {
            editor.restoreFromMemento(history.undo());
            editor.displayState();
        }
        
        // Demonstrate redo
        System.out.println("\n--- Redo Operations ---");
        if (history.canRedo()) {
            editor.restoreFromMemento(history.redo());
            editor.displayState();
        }
        
        // Show history
        System.out.println("\n--- History Information ---");
        history.showHistory();
    }
}

Debugging Demonstration

Show common errors and solutions:

// Common mistake: Exposing memento internals
class BadMemento {
    public String content;  // ERROR: Public field breaks encapsulation!
    public int cursor;      // ERROR: Anyone can modify this!
    
    // This allows external modification and breaks the pattern
}

// Correct approach: Encapsulated memento
class GoodMemento {
    private final String content;     // Private and immutable
    private final int cursor;         // Only originator can access
    
    GoodMemento(String content, int cursor) {
        this.content = content;
        this.cursor = cursor;
    }
    
    // Package-private accessors for originator only
    String getContent() { return content; }
    int getCursor() { return cursor; }
}

// Common mistake: Caretaker examining memento contents
class BadCaretaker {
    public void examineMemento(TextMemento memento) {
        // ERROR: This breaks encapsulation!
        // String content = memento.getContent(); // Won't compile due to package-private access
        // This is good - caretaker shouldn't access memento internals
    }
}

6. Hands-On Practice (30-40 minutes)

Exercise 1: Calculator with Operation History (10 minutes)

Objective: Implement a calculator that can undo/redo mathematical operations

Instructions:

  1. Create a Calculator class with basic arithmetic operations
  2. Implement a CalculatorMemento that stores the current value and operation history
  3. Add undo/redo functionality for individual operations

Starter Code:

class Calculator {
    private double currentValue = 0;
    private List<String> operationHistory = new ArrayList<>();
    
    public void add(double value) {
        // TODO: Implement addition and save to history
    }
    
    public void subtract(double value) {
        // TODO: Implement subtraction
    }
    
    // TODO: Add createMemento and restoreFromMemento methods
}

class CalculatorMemento {
    // TODO: Store calculator state
}

class CalculatorHistory {
    // TODO: Manage calculator mementos
}

Solution:

import java.util.*;

class CalculatorMemento {
    private final double value;
    private final List<String> operations;
    private final long timestamp;
    
    CalculatorMemento(double value, List<String> operations) {
        this.value = value;
        this.operations = new ArrayList<>(operations); // Defensive copy
        this.timestamp = System.currentTimeMillis();
    }
    
    double getValue() { return value; }
    List<String> getOperations() { return new ArrayList<>(operations); }
    
    public String getInfo() {
        return String.format("Value: %.2f, Operations: %d, Time: %s", 
                           value, operations.size(), new Date(timestamp));
    }
}

class Calculator {
    private double currentValue = 0;
    private List<String> operationHistory = new ArrayList<>();
    
    public void add(double value) {
        currentValue += value;
        operationHistory.add("+" + value);
        System.out.printf("Added %.2f, Result: %.2f%n", value, currentValue);
    }
    
    public void subtract(double value) {
        currentValue -= value;
        operationHistory.add("-" + value);
        System.out.printf("Subtracted %.2f, Result: %.2f%n", value, currentValue);
    }
    
    public void multiply(double value) {
        currentValue *= value;
        operationHistory.add("*" + value);
        System.out.printf("Multiplied by %.2f, Result: %.2f%n", value, currentValue);
    }
    
    public void divide(double value) {
        if (value != 0) {
            currentValue /= value;
            operationHistory.add("/" + value);
            System.out.printf("Divided by %.2f, Result: %.2f%n", value, currentValue);
        } else {
            System.out.println("Cannot divide by zero!");
        }
    }
    
    public void clear() {
        currentValue = 0;
        operationHistory.clear();
        System.out.println("Calculator cleared");
    }
    
    public CalculatorMemento createMemento() {
        return new CalculatorMemento(currentValue, operationHistory);
    }
    
    public void restoreFromMemento(CalculatorMemento memento) {
        if (memento != null) {
            this.currentValue = memento.getValue();
            this.operationHistory = memento.getOperations();
            System.out.println("Calculator restored: " + memento.getInfo());
        }
    }
    
    public void displayState() {
        System.out.printf("Current Value: %.2f%n", currentValue);
        System.out.println("Operation History: " + operationHistory);
    }
}

class CalculatorHistory {
    private final Stack<CalculatorMemento> history = new Stack<>();
    private final int maxSize;
    
    public CalculatorHistory(int maxSize) {
        this.maxSize = maxSize;
    }
    
    public void saveState(CalculatorMemento memento) {
        history.push(memento);
        if (history.size() > maxSize) {
            history.remove(0);
        }
        System.out.println("State saved. History size: " + history.size());
    }
    
    public CalculatorMemento undo() {
        if (history.size() > 1) {
            history.pop(); // Remove current state
            return history.peek(); // Return previous state
        }
        System.out.println("Cannot undo - no previous state");
        return null;
    }
    
    public boolean canUndo() {
        return history.size() > 1;
    }
}

Exercise 2: Game Character State Management (15 minutes)

Objective: Create a game character that can save and restore its state (position, health, inventory)

Requirements:

  • Character has position (x, y), health points, and an inventory of items
  • Implement checkpoint save/load functionality
  • Support multiple save slots
  • Handle inventory modifications properly

Hints:

  • Consider deep copying for complex state like inventory
  • Think about what constitutes the complete character state

Exercise 3: Document Version Control System (15 minutes)

Objective: Build a simplified version control system for documents

Challenge: Implement a system that supports:

  • Document creation and modification tracking
  • Branch-like functionality with named checkpoints
  • Diff generation between versions
  • Merge conflict detection for simultaneous edits

Extension Ideas:

  • Add compression for large document snapshots
  • Implement incremental deltas instead of full snapshots
  • Add collaborative editing features

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Memory leaks due to unlimited memento storage Cause: Not implementing proper history size limits or cleanup mechanisms Solution: Implement maximum history sizes, use weak references where appropriate, and provide cleanup methods

  2. Problem: Breaking encapsulation by exposing memento internals Cause: Making memento fields public or providing unrestricted access methods Solution: Use package-private access, immutable data structures, and defensive copying

  3. Problem: Performance issues with large object snapshots Cause: Capturing complete object state for every operation, even minor changes Solution: Implement incremental snapshots, compression, or command-pattern based undo systems for frequent changes

  4. Problem: Inconsistent state after partial restoration failures Cause: Not handling exceptions during state restoration properly Solution: Implement atomic restoration operations and validate memento integrity before restoration

Debugging Tips

  • State Validation: Verify that restored objects are in valid, consistent states
  • Memory Monitoring: Track memento storage growth and implement alerts for excessive usage
  • Serialization Testing: If using serialization, test with different object versions and handle compatibility issues
  • Thread Safety: Ensure memento creation and restoration are thread-safe in concurrent environments

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Immutable Mementos: Design mementos as immutable objects to prevent accidental modification
  • Efficient Storage: Use appropriate data structures and compression for large state snapshots
  • Version Compatibility: Handle schema evolution when mementos need to persist across application versions

Performance Optimizations

  • Lazy Copying: Only copy state that actually changes between snapshots
  • Compressed Storage: Use compression algorithms for large state objects
  • Incremental Snapshots: Store only differences from previous snapshots when appropriate

Next Steps in Your Learning Journey

  • Command Pattern: Study how command and memento patterns work together for comprehensive undo systems
  • Event Sourcing: Learn about storing events instead of state snapshots for audit trails and reconstruction
  • Persistence Patterns: Explore database snapshot and backup strategies for large-scale applications

9. Summary and Review (5 minutes)

Key Takeaways

  1. Encapsulation Preservation: Memento pattern enables state management without breaking object encapsulation
  2. Flexible State Management: The pattern supports sophisticated undo/redo systems and checkpointing
  3. Memory Awareness: Effective implementations balance functionality with memory usage and performance

Quick Review Questions

  1. How does the memento pattern maintain encapsulation while enabling state restoration?
  2. What’s the difference between the memento pattern and simple object serialization?
  3. How would you handle memory constraints in a system with frequent state snapshots?

For Instructors

Teaching Tips

  • Interactive Demonstrations: Use text editors or simple drawing applications to show memento patterns in action
  • Memory Visualization: Show actual memory usage before and after implementing efficient memento strategies
  • Real-World Examples: Discuss version control systems, database transactions, and game save systems as practical applications

Common Student Questions and Responses

  • “Why not just make fields public for easy state access?” - Demonstrate how this breaks encapsulation and creates maintenance problems
  • “How is this different from just cloning objects?” - Explain controlled state capture vs complete object duplication
  • “What about performance overhead?” - Discuss trade-offs and optimization strategies like incremental snapshots

Assessment Ideas

  • Undo System Implementation: Have students build complete undo/redo systems for different applications
  • Performance Analysis: Measure and optimize memory usage in memento-based systems
  • Pattern Integration: Combine memento with command pattern for comprehensive action management

Adaptation Notes

  • For Beginners: Start with simple state objects before moving to complex hierarchical state
  • For Advanced Students: Discuss advanced topics like event sourcing, distributed state management, and persistence strategies
  • Industry Focus: Adapt examples to specific domains (web development, game development, enterprise applications)