Memento

The Memento pattern is a behavioral design pattern that provides the ability to restore an object to its previous state without violating encapsulation principles. This pattern is particularly useful when you need to implement undo functionality, checkpoints, or snapshots in your application.

Core Concept

The Memento pattern captures and externalizes an object’s internal state so that the object can be restored to this state later. The key insight is that only the originator object should have access to the memento’s contents, preserving encapsulation while enabling state restoration.

Pattern Structure

┌─────────────┐    creates    ┌─────────────┐
│             │──────────────→│             │
│ Originator  │               │  Memento    │
│             │←──────────────│             │
└─────────────┘   restores    └─────────────┘
       △                             │
       │                             │
       │ manages                     │ stores
       │                             │
       ▽                             ▽
┌─────────────┐               ┌─────────────┐
│             │──────────────→│             │
│ Caretaker   │    stores     │   State     │
│             │               │ Information │
└─────────────┘               └─────────────┘

Key Components

Originator: The object whose state needs to be saved and restored. It creates mementos containing snapshots of its current state and uses mementos to restore its previous state.

Memento: A value object that acts as a snapshot of the originator’s state. It should be immutable and only accessible to the originator that created it.

Caretaker: Manages mementos but never operates on or examines their contents. It’s responsible for storing mementos and providing them back to the originator when restoration is needed.

When to Use

The Memento pattern is ideal when you need to:

  • Implement undo/redo functionality in applications
  • Create snapshots or checkpoints of object states
  • Provide rollback capabilities in transactional systems
  • Save and restore game states
  • Implement version control systems for objects

Benefits and Trade-offs

Benefits:

  • Preserves encapsulation boundaries
  • Simplifies the originator by delegating state management
  • Provides clean separation of concerns

Trade-offs:

  • Can be memory-intensive if mementos are large or numerous
  • Caretaker lifecycle management becomes important
  • May impact performance with frequent state captures

Example 1: Text Editor with Undo Functionality

This example demonstrates the Memento pattern through a text editor that supports undo and redo operations. The TextEditor class acts as the originator, creating snapshots of its state including text content and cursor position. The TextMemento is implemented as an immutable dataclass that captures the editor’s state. The EditorHistory class serves as the caretaker, managing a collection of mementos and providing undo/redo functionality.

from typing import List, Optional
from dataclasses import dataclass
from datetime import datetime

@dataclass(frozen=True)
class TextMemento:
    """
    Memento class that stores the state of a text editor.
    Frozen dataclass ensures immutability.
    """
    content: str
    cursor_position: int
    timestamp: datetime

class TextEditor:
    """
    Originator class that maintains text content and can create/restore mementos.
    """
    
    def __init__(self) -> None:
        self._content: str = ""
        self._cursor_position: int = 0
    
    def write(self, text: str) -> None:
        """Insert text at current cursor position."""
        self._content = (self._content[:self._cursor_position] + 
                        text + 
                        self._content[self._cursor_position:])
        self._cursor_position += len(text)
    
    def delete(self, length: int) -> None:
        """Delete specified number of characters before cursor."""
        start_pos = max(0, self._cursor_position - length)
        self._content = (self._content[:start_pos] + 
                        self._content[self._cursor_position:])
        self._cursor_position = start_pos
    
    def set_cursor_position(self, position: int) -> None:
        """Move cursor to specified position."""
        self._cursor_position = max(0, min(position, len(self._content)))
    
    def create_memento(self) -> TextMemento:
        """Create a snapshot of current editor state."""
        return TextMemento(
            content=self._content,
            cursor_position=self._cursor_position,
            timestamp=datetime.now()
        )
    
    def restore_from_memento(self, memento: TextMemento) -> None:
        """Restore editor state from a memento."""
        self._content = memento.content
        self._cursor_position = memento.cursor_position
    
    def get_content(self) -> str:
        """Get current text content."""
        return self._content
    
    def get_cursor_position(self) -> int:
        """Get current cursor position."""
        return self._cursor_position
    
    def display(self) -> str:
        """Display content with cursor position marked."""
        content_with_cursor = (self._content[:self._cursor_position] + 
                              "|" + 
                              self._content[self._cursor_position:])
        return content_with_cursor

class EditorHistory:
    """
    Caretaker class that manages editor state history for undo functionality.
    """
    
    def __init__(self, editor: TextEditor) -> None:
        self._editor = editor
        self._history: List[TextMemento] = []
        self._current_index: int = -1
    
    def save_state(self) -> None:
        """Save current editor state to history."""
        # Remove any future states if we're not at the end
        self._history = self._history[:self._current_index + 1]
        
        # Add new state
        memento = self._editor.create_memento()
        self._history.append(memento)
        self._current_index += 1
        
        # Optional: Limit history size to prevent memory issues
        max_history_size = 50
        if len(self._history) > max_history_size:
            self._history.pop(0)
            self._current_index -= 1
    
    def undo(self) -> bool:
        """Undo last operation by restoring previous state."""
        if self._current_index > 0:
            self._current_index -= 1
            memento = self._history[self._current_index]
            self._editor.restore_from_memento(memento)
            return True
        return False
    
    def redo(self) -> bool:
        """Redo next operation by restoring next state."""
        if self._current_index < len(self._history) - 1:
            self._current_index += 1
            memento = self._history[self._current_index]
            self._editor.restore_from_memento(memento)
            return True
        return False
    
    def can_undo(self) -> bool:
        """Check if undo operation is possible."""
        return self._current_index > 0
    
    def can_redo(self) -> bool:
        """Check if redo operation is possible."""
        return self._current_index < len(self._history) - 1
    
    def get_history_info(self) -> str:
        """Get formatted history information for debugging."""
        info = f"History size: {len(self._history)}\n"
        info += f"Current index: {self._current_index}\n"
        info += f"Can undo: {self.can_undo()}\n"
        info += f"Can redo: {self.can_redo()}\n"
        return info

# Usage demonstration
if __name__ == "__main__":
    # Create text editor and history manager
    editor = TextEditor()
    history = EditorHistory(editor)
    
    # Initial state
    history.save_state()
    print("Initial state:", editor.display())
    
    # Make some changes
    editor.write("Hello")
    history.save_state()
    print("After writing 'Hello':", editor.display())
    
    editor.write(" World")
    history.save_state()
    print("After writing ' World':", editor.display())
    
    editor.set_cursor_position(5)  # Move cursor between "Hello" and " World"
    editor.write(" Python")
    history.save_state()
    print("After inserting ' Python':", editor.display())
    
    # Demonstrate undo functionality
    print("\n--- Undo Operations ---")
    while history.can_undo():
        history.undo()
        print("After undo:", editor.display())
    
    # Demonstrate redo functionality
    print("\n--- Redo Operations ---")
    while history.can_redo():
        history.redo()
        print("After redo:", editor.display())

Key aspects of this implementation:

  • Encapsulation: The memento only exposes necessary data and remains immutable
  • State Management: The caretaker handles complex history navigation logic
  • Memory Management: History size is limited to prevent excessive memory usage
  • User Experience: Provides intuitive undo/redo operations familiar to users

Example 2: Game State Management System

This example showcases the Memento pattern in a game context where state preservation is crucial for features like checkpoints, save/load functionality, and recovery from failure states. The Game class maintains complex state including player progress, inventory, enemy positions, and game status. The GameMemento captures comprehensive snapshots while the GameStateManager provides sophisticated state management capabilities.

from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass
from enum import Enum
import json

class GameStatus(Enum):
    """Enumeration of possible game states."""
    PLAYING = "playing"
    PAUSED = "paused"
    GAME_OVER = "game_over"

@dataclass(frozen=True)
class GameMemento:
    """
    Memento storing complete game state snapshot.
    Includes all necessary data to restore game state.
    """
    level: int
    score: int
    lives: int
    player_position: Tuple[int, int]
    enemies: List[Tuple[int, int]]
    items: Dict[str, int]  # item_type -> quantity
    game_status: GameStatus
    timestamp: float

class Game:
    """
    Originator class representing a game that can save and restore its state.
    """
    
    def __init__(self) -> None:
        # Game state variables
        self._level: int = 1
        self._score: int = 0
        self._lives: int = 3
        self._player_position: Tuple[int, int] = (0, 0)
        self._enemies: List[Tuple[int, int]] = []
        self._items: Dict[str, int] = {"health_potion": 0, "key": 0, "coin": 0}
        self._game_status: GameStatus = GameStatus.PLAYING
    
    # Game action methods
    def move_player(self, x: int, y: int) -> None:
        """Move player to new position."""
        self._player_position = (x, y)
    
    def add_score(self, points: int) -> None:
        """Add points to current score."""
        self._score += points
    
    def lose_life(self) -> None:
        """Decrease lives by 1."""
        self._lives = max(0, self._lives - 1)
        if self._lives == 0:
            self._game_status = GameStatus.GAME_OVER
    
    def gain_life(self) -> None:
        """Increase lives by 1."""
        self._lives += 1
    
    def level_up(self) -> None:
        """Advance to next level."""
        self._level += 1
        # Reset enemies for new level
        self._enemies = self._generate_enemies()
    
    def add_item(self, item_type: str, quantity: int = 1) -> None:
        """Add items to inventory."""
        if item_type in self._items:
            self._items[item_type] += quantity
        else:
            self._items[item_type] = quantity
    
    def use_item(self, item_type: str, quantity: int = 1) -> bool:
        """Use items from inventory. Returns True if successful."""
        if item_type in self._items and self._items[item_type] >= quantity:
            self._items[item_type] -= quantity
            return True
        return False
    
    def add_enemy(self, position: Tuple[int, int]) -> None:
        """Add enemy at specified position."""
        self._enemies.append(position)
    
    def remove_enemy(self, position: Tuple[int, int]) -> bool:
        """Remove enemy at specified position."""
        try:
            self._enemies.remove(position)
            return True
        except ValueError:
            return False
    
    def pause_game(self) -> None:
        """Pause the game."""
        if self._game_status == GameStatus.PLAYING:
            self._game_status = GameStatus.PAUSED
    
    def resume_game(self) -> None:
        """Resume the game."""
        if self._game_status == GameStatus.PAUSED:
            self._game_status = GameStatus.PLAYING
    
    def _generate_enemies(self) -> List[Tuple[int, int]]:
        """Generate enemies for current level."""
        # Simple enemy generation based on level
        enemies = []
        for i in range(self._level * 2):  # More enemies each level
            enemies.append((i * 10, i * 5))
        return enemies
    
    # Memento pattern implementation
    def create_memento(self) -> GameMemento:
        """Create snapshot of current game state."""
        import time
        return GameMemento(
            level=self._level,
            score=self._score,
            lives=self._lives,
            player_position=self._player_position,
            enemies=self._enemies.copy(),  # Create copy to avoid reference issues
            items=self._items.copy(),
            game_status=self._game_status,
            timestamp=time.time()
        )
    
    def restore_from_memento(self, memento: GameMemento) -> None:
        """Restore game state from memento."""
        self._level = memento.level
        self._score = memento.score
        self._lives = memento.lives
        self._player_position = memento.player_position
        self._enemies = list(memento.enemies)  # Create new list
        self._items = dict(memento.items)      # Create new dict
        self._game_status = memento.game_status
    
    # Getter methods for displaying state
    def get_game_info(self) -> str:
        """Get formatted game information."""
        info = f"""
Game Status: {self._game_status.value.title()}
Level: {self._level}
Score: {self._score}
Lives: {self._lives}
Player Position: {self._player_position}
Enemies: {len(self._enemies)} enemies
Items: {', '.join(f'{k}: {v}' for k, v in self._items.items())}
        """.strip()
        return info

class GameStateManager:
    """
    Caretaker class managing game state checkpoints and save/load functionality.
    """
    
    def __init__(self, game: Game) -> None:
        self._game = game
        self._checkpoints: List[GameMemento] = []
        self._saved_games: Dict[str, GameMemento] = {}
    
    def create_checkpoint(self) -> None:
        """Create a checkpoint of current game state."""
        checkpoint = self._game.create_memento()
        self._checkpoints.append(checkpoint)
        
        # Limit checkpoint history to prevent memory issues
        max_checkpoints = 10
        if len(self._checkpoints) > max_checkpoints:
            self._checkpoints.pop(0)
        
        print(f"Checkpoint created at level {checkpoint.level}")
    
    def restore_last_checkpoint(self) -> bool:
        """Restore game to most recent checkpoint."""
        if self._checkpoints:
            latest_checkpoint = self._checkpoints[-1]
            self._game.restore_from_memento(latest_checkpoint)
            print(f"Restored to checkpoint at level {latest_checkpoint.level}")
            return True
        print("No checkpoints available")
        return False
    
    def save_game(self, save_name: str) -> None:
        """Save current game state with a name."""
        memento = self._game.create_memento()
        self._saved_games[save_name] = memento
        print(f"Game saved as '{save_name}' at level {memento.level}")
    
    def load_game(self, save_name: str) -> bool:
        """Load game state by name."""
        if save_name in self._saved_games:
            memento = self._saved_games[save_name]
            self._game.restore_from_memento(memento)
            print(f"Game '{save_name}' loaded (level {memento.level})")
            return True
        print(f"No saved game found with name '{save_name}'")
        return False
    
    def list_saves(self) -> None:
        """Display all saved games."""
        if not self._saved_games:
            print("No saved games")
            return
        
        print("Saved Games:")
        print("-" * 40)
        for save_name, memento in self._saved_games.items():
            print(f"{save_name:15} | Level {memento.level:2} | Score {memento.score:6}")
    
    def get_checkpoint_info(self) -> str:
        """Get information about available checkpoints."""
        if not self._checkpoints:
            return "No checkpoints available"
        
        info = f"Checkpoints available: {len(self._checkpoints)}\n"
        info += "Recent checkpoints:\n"
        for i, checkpoint in enumerate(self._checkpoints[-3:], 1):  # Show last 3
            info += f"  {i}. Level {checkpoint.level}, Score {checkpoint.score}\n"
        return info.rstrip()

# Usage demonstration
if __name__ == "__main__":
    # Create game and state manager
    game = Game()
    state_manager = GameStateManager(game)
    
    print("=== Game State Management Demo ===\n")
    
    # Initial game state
    print("Initial game state:")
    print(game.get_game_info())
    
    # Play some game actions
    print("\n--- Playing the game ---")
    game.move_player(10, 20)
    game.add_score(100)
    game.add_item("coin", 5)
    game.add_item("health_potion", 2)
    game.add_enemy((50, 30))
    
    # Create checkpoint
    state_manager.create_checkpoint()
    print("\nAfter some gameplay:")
    print(game.get_game_info())
    
    # Continue playing
    print("\n--- More gameplay ---")
    game.level_up()
    game.add_score(250)
    game.move_player(100, 150)
    game.lose_life()
    
    # Save game
    state_manager.save_game("before_boss_fight")
    print("\nAfter leveling up:")
    print(game.get_game_info())
    
    # Simulate difficult section
    print("\n--- Difficult section ---")
    game.lose_life()
    game.lose_life()  # Game over
    print("\nAfter losing lives:")
    print(game.get_game_info())
    
    # Restore from checkpoint
    print("\n--- Restoring from checkpoint ---")
    state_manager.restore_last_checkpoint()
    print("After checkpoint restore:")
    print(game.get_game_info())
    
    # Show save management
    print("\n--- Save Management ---")
    state_manager.save_game("good_progress")
    state_manager.save_game("backup_save")
    state_manager.list_saves()
    
    print("\n" + state_manager.get_checkpoint_info())

Notable features of this implementation:

  • Complex State Capture: Handles multiple data types including collections and enums
  • Multiple Restoration Points: Supports both temporary checkpoints and permanent saves
  • Memory Management: Limits checkpoint history to prevent resource exhaustion
  • User-Friendly Interface: Provides named save slots and information about available saves
  • State Integrity: Creates defensive copies to prevent unintended state sharing

The Memento pattern proves invaluable in scenarios requiring reliable state management, offering a clean separation between state capture, storage, and restoration while maintaining proper encapsulation boundaries.

Track your progress

Mark this subtopic as completed when you finish reading.