Overview
The Memento Pattern addresses the problem of capturing an object's state for later restoration without exposing its internal structure or violating encapsulation principles. The pattern establishes a clear separation between the object being saved (originator), the saved state itself (memento), and the entity managing save operations (caretaker).
The pattern emerged from the need to implement undo mechanisms, checkpoints, and state rollback features in applications. Traditional approaches to saving object state often required either making internal state public or creating tight coupling between objects and their storage mechanisms. The Memento Pattern solves this by encapsulating state capture within the originator object itself, producing an opaque memento object that other components can store and return without understanding its contents.
Three primary participants define the pattern structure. The Originator creates mementos containing snapshots of its current state and uses mementos to restore previous states. The Memento stores the originator's internal state and protects against access by objects other than the originator. The Caretaker manages memento storage, determining when to capture states and when to restore them, but never examines or modifies memento contents.
The pattern's value appears most clearly in situations requiring state history tracking, transactional operations with rollback capability, or complex undo/redo systems. Applications range from text editors maintaining edit history to game engines implementing save points, from database transaction systems to configuration management tools.
Consider a text editor example where users need undo functionality:
class TextEditor
attr_reader :content
def initialize
@content = ""
end
def write(text)
@content += text
end
def save
EditorMemento.new(@content)
end
def restore(memento)
@content = memento.state
end
end
class EditorMemento
attr_reader :state
def initialize(state)
@state = state
end
end
The pattern maintains clear boundaries between concerns: the editor manages content, mementos store state snapshots, and a separate history manager controls save/restore timing without accessing internal editor details.
Key Principles
The Memento Pattern operates on three fundamental principles: encapsulation preservation, state externalization, and separation of concerns. Each principle addresses specific challenges in state management while maintaining object-oriented design integrity.
Encapsulation Preservation ensures that saving and restoring object state does not expose internal implementation details. The originator produces mementos through controlled interfaces, placing state data inside memento objects without making that data accessible to external callers. Only the originator can extract state from its own mementos. This principle prevents the fragile couplings that arise when external code depends on internal state representations.
The originator controls what state gets saved and how. It might save complete state or selective subsets depending on requirements. The memento acts as an opaque token to external code:
class DatabaseConnection
def initialize(host, port, credentials)
@host = host
@port = port
@credentials = credentials
@transaction_state = nil
end
def create_memento
ConnectionMemento.new(@host, @port, @credentials, @transaction_state)
end
def restore_memento(memento)
@host = memento.host
@port = memento.port
@credentials = memento.credentials
@transaction_state = memento.transaction_state
end
private
class ConnectionMemento
attr_reader :host, :port, :credentials, :transaction_state
def initialize(host, port, credentials, transaction_state)
@host = host
@port = port
@credentials = credentials
@transaction_state = transaction_state
end
end
end
External code receives ConnectionMemento instances but cannot access their internal attributes directly due to the private class definition. Only the originating DatabaseConnection can use these mementos.
State Externalization moves state storage outside the originator object. The originator creates self-contained memento objects that exist independently in memory. This externalization enables multiple snapshots to coexist, supports state transfer between object instances, and facilitates serialization of captured states.
Mementos represent immutable state snapshots. Once created, memento contents do not change. This immutability prevents accidental state corruption and ensures that restoring a memento produces predictable results. If state needs updating, the originator creates a new memento rather than modifying existing ones:
class ShoppingCart
def initialize
@items = []
@discounts = []
end
def add_item(item)
@items << item
end
def apply_discount(discount)
@discounts << discount
end
def checkpoint
CartMemento.new(@items.dup, @discounts.dup)
end
def restore(memento)
@items = memento.items.dup
@discounts = memento.discounts.dup
end
end
class CartMemento
attr_reader :items, :discounts
def initialize(items, discounts)
@items = items.freeze
@discounts = discounts.freeze
end
end
The checkpoint method creates independent copies of internal state, and the memento freezes these copies to enforce immutability. Restoration creates new copies rather than reusing references, preventing shared state mutations.
Separation of Concerns divides responsibilities across three distinct roles. The originator focuses on its core functionality and state management. Mementos serve as passive state containers with no behavior beyond storage. Caretakers handle memento lifecycle without understanding state contents.
This separation enables flexible storage strategies. A caretaker might maintain a simple history stack, implement sophisticated undo/redo trees, persist mementos to disk, or transfer them across network boundaries. The originator remains unaware of these storage details:
class CommandHistory
def initialize
@history = []
@current_position = -1
end
def save(memento)
# Remove any states after current position (redo history)
@history = @history[0..@current_position]
@history << memento
@current_position += 1
end
def undo
return nil if @current_position <= 0
@current_position -= 1
@history[@current_position]
end
def redo
return nil if @current_position >= @history.length - 1
@current_position += 1
@history[@current_position]
end
end
The CommandHistory caretaker implements undo/redo logic without knowing what states it manages. It treats mementos as opaque objects, focusing solely on their organization and retrieval.
Design Considerations
The Memento Pattern suits specific scenarios while presenting challenges in others. Selection requires analyzing state complexity, frequency of state capture, and memory constraints.
State Complexity and Granularity determine whether the pattern provides value. Objects with simple state (few primitive attributes) might not benefit from the pattern's overhead. Objects with complex state graphs, numerous interconnected components, or intricate internal structures gain more value from formalized state capture. The pattern excels when state representation differs significantly from external interfaces.
Consider an object graph where state includes relationships between multiple entities. Direct state access would require understanding these relationships. The Memento Pattern lets the originator manage relationship serialization internally:
class WorkflowEngine
def initialize
@steps = []
@transitions = {}
@current_step = nil
@context_data = {}
end
def create_checkpoint
WorkflowMemento.new(
steps: @steps.map(&:dup),
transitions: @transitions.dup,
current_step: @current_step,
context: deep_copy(@context_data)
)
end
def restore_checkpoint(memento)
@steps = memento.steps
@transitions = memento.transitions
@current_step = memento.current_step
@context_data = memento.context
end
private
def deep_copy(obj)
Marshal.load(Marshal.dump(obj))
end
end
The workflow engine maintains complex internal state that external code should not manipulate directly. The memento encapsulates this complexity.
Frequency of State Capture impacts performance and memory usage. Applications requiring frequent snapshots (every keystroke in a text editor) need efficient memento implementations. Infrequent captures (game save points) tolerate more overhead. High-frequency scenarios might use incremental mementos storing only state deltas rather than complete snapshots.
Memory consumption multiplies with snapshot count. An application saving state every second accumulates 3,600 mementos per hour. If each memento consumes 1KB, hourly memory usage reaches 3.6MB. Applications must implement memento pruning strategies:
class MemoryEfficientCaretaker
def initialize(max_history: 50)
@max_history = max_history
@mementos = []
@pruned_count = 0
end
def save(memento)
@mementos << memento
if @mementos.size > @max_history
@mementos.shift
@pruned_count += 1
end
end
def stats
{
current_history: @mementos.size,
pruned_total: @pruned_count,
oldest_timestamp: @mementos.first&.timestamp
}
end
end
Memory limits force trade-offs between history depth and memory consumption. Applications might combine recent fine-grained snapshots with older coarse-grained snapshots.
Encapsulation vs. Performance creates tension in pattern implementation. Strict encapsulation requires mementos to be completely opaque, forcing deep copies of all state. Performance optimization might suggest sharing immutable state or using references to unchanged portions. This trade-off becomes critical for objects with large state spaces.
Ruby's object model provides flexibility in managing this trade-off. Private classes create strong encapsulation. Friend-like access patterns using module inclusion offer controlled flexibility. Freezing objects prevents mutations while enabling reference sharing:
class DataProcessor
def initialize(config)
@config = config # Large configuration object
@working_data = []
@metadata = {}
end
def snapshot
ProcessorMemento.new(
config: @config, # Share reference (config is immutable)
working_data: @working_data.dup, # Copy (mutable)
metadata: @metadata.dup # Copy (mutable)
)
end
end
The snapshot shares the configuration reference since that data never changes, reducing memory overhead. Mutable working data receives full copies, maintaining state independence.
Alternative Patterns address similar problems with different trade-offs. The Command Pattern with execute/undo methods encapsulates state changes rather than states themselves. This approach works well for operations with simple inverse actions but struggles with complex state or operations without clear inverses.
Serialization mechanisms like JSON or YAML export provide external state representation but sacrifice encapsulation by exposing internal structure. Version control systems offer sophisticated state management but require external infrastructure.
The Strategy Pattern combined with state management provides flexibility when different save/restore strategies suit different contexts. The pattern selection depends on whether states require complex capture logic or simple serialization suffices.
Ruby Implementation
Ruby's object model and metaprogramming capabilities support multiple memento implementation approaches. The language's flexible access control, first-class modules, and duck typing influence pattern design.
Basic Implementation with Private Classes uses nested class definitions to restrict memento access:
class GameCharacter
attr_reader :name, :level
def initialize(name)
@name = name
@level = 1
@health = 100
@mana = 50
@inventory = []
@position = { x: 0, y: 0 }
end
def gain_experience(points)
@level += points / 100
end
def take_damage(amount)
@health -= amount
end
def move(x, y)
@position = { x: x, y: y }
end
def save_state
CharacterMemento.new(
level: @level,
health: @health,
mana: @mana,
inventory: @inventory.dup,
position: @position.dup
)
end
def restore_state(memento)
@level = memento.level
@health = memento.health
@mana = memento.mana
@inventory = memento.inventory.dup
@position = memento.position.dup
end
class CharacterMemento
attr_reader :level, :health, :mana, :inventory, :position
def initialize(level:, health:, mana:, inventory:, position:)
@level = level
@health = health
@mana = mana
@inventory = inventory.freeze
@position = position.freeze
@timestamp = Time.now
end
def age
Time.now - @timestamp
end
end
private_constant :CharacterMemento
end
The private_constant declaration prevents external code from directly instantiating CharacterMemento while allowing GameCharacter to create and use mementos. External code treats mementos as opaque objects.
Module-Based Access Control creates friend-like relationships using Ruby modules:
module MementoProtocol
class StateSnapshot
def initialize(data, originator_class)
@data = data
@originator_class = originator_class
@created_at = Time.now
end
def restore_to(originator)
unless originator.is_a?(@originator_class)
raise TypeError, "Cannot restore to different class"
end
originator.restore_from_snapshot(@data)
end
def metadata
{
originator: @originator_class.name,
created: @created_at,
age: Time.now - @created_at
}
end
protected
attr_reader :data
end
end
class FormState
include MementoProtocol
def initialize
@fields = {}
@validations = {}
end
def set_field(name, value)
@fields[name] = value
end
def create_snapshot
StateSnapshot.new(
{
fields: @fields.dup,
validations: @validations.dup
},
self.class
)
end
protected
def restore_from_snapshot(data)
@fields = data[:fields].dup
@validations = data[:validations].dup
end
end
The module defines shared memento infrastructure. Protected methods establish controlled access patterns between originators and their mementos.
Marshal-Based Deep Copying handles complex object graphs automatically:
class DocumentEditor
def initialize
@paragraphs = []
@styles = {}
@metadata = {}
@undo_stack = []
end
def save_checkpoint
state_data = {
paragraphs: @paragraphs,
styles: @styles,
metadata: @metadata
}
DocumentMemento.new(Marshal.dump(state_data))
end
def restore_checkpoint(memento)
state_data = Marshal.load(memento.serialized_state)
@paragraphs = state_data[:paragraphs]
@styles = state_data[:styles]
@metadata = state_data[:metadata]
end
class DocumentMemento
attr_reader :serialized_state
def initialize(serialized_state)
@serialized_state = serialized_state
@timestamp = Time.now
end
def byte_size
@serialized_state.bytesize
end
end
end
Marshal serialization creates complete copies including nested objects. This approach handles circular references and complex object graphs but requires all objects to be Marshal-compatible.
Lazy State Capture defers full state copying until necessary:
class DatabaseTransaction
def initialize
@original_records = {}
@modified_records = {}
@deleted_ids = []
end
def modify(id, attributes)
capture_original(id) unless @original_records.key?(id)
@modified_records[id] = attributes
end
def delete(id)
capture_original(id) unless @original_records.key?(id)
@deleted_ids << id
end
def create_savepoint
TransactionMemento.new(
originals: @original_records.dup,
modified: @modified_records.dup,
deleted: @deleted_ids.dup
)
end
def rollback_to(memento)
memento.originals.each do |id, record|
restore_record(id, record)
end
@modified_records = {}
@deleted_ids = []
end
private
def capture_original(id)
@original_records[id] = fetch_current_state(id)
end
def fetch_current_state(id)
# Retrieve current record state
{ id: id, data: "current_state" }
end
def restore_record(id, record)
# Restore record to saved state
end
class TransactionMemento
attr_reader :originals, :modified, :deleted
def initialize(originals:, modified:, deleted:)
@originals = originals
@modified = modified
@deleted = deleted
end
end
end
Lazy capture minimizes overhead by saving only changed state. The transaction captures original values when modifications occur, not preemptively.
Struct-Based Lightweight Mementos provide efficient implementations for simple state:
class Counter
def initialize
@count = 0
@increment_history = []
end
def increment(amount = 1)
@count += amount
@increment_history << amount
end
def snapshot
CounterSnapshot.new(@count, @increment_history.dup)
end
def restore(snapshot)
@count = snapshot.count
@increment_history = snapshot.history.dup
end
CounterSnapshot = Struct.new(:count, :history) do
def total_increments
history.sum
end
def increment_count
history.size
end
end
end
Struct provides lightweight value objects with named attributes. This approach suits simple state without complex behavior requirements.
Practical Examples
Text Editor with Undo/Redo demonstrates multi-level state management:
class TextDocument
attr_reader :content
def initialize
@content = ""
@cursor_position = 0
@selection = nil
end
def insert_text(text)
@content.insert(@cursor_position, text)
@cursor_position += text.length
end
def delete_selection(start_pos, end_pos)
@content[start_pos...end_pos] = ""
@cursor_position = start_pos
end
def move_cursor(position)
@cursor_position = [0, [position, @content.length].min].max
end
def create_memento
DocumentMemento.new(
content: @content.dup,
cursor: @cursor_position,
selection: @selection&.dup
)
end
def restore_from_memento(memento)
@content = memento.content.dup
@cursor_position = memento.cursor
@selection = memento.selection&.dup
end
class DocumentMemento
attr_reader :content, :cursor, :selection
def initialize(content:, cursor:, selection:)
@content = content
@cursor = cursor
@selection = selection
@timestamp = Time.now
end
end
end
class UndoRedoManager
def initialize(document)
@document = document
@history = [document.create_memento]
@current_index = 0
end
def execute_command
# Save state before executing command
yield
# Remove any states after current position
@history = @history[0..@current_index]
# Add new state
@history << @document.create_memento
@current_index = @history.length - 1
end
def undo
return false if @current_index <= 0
@current_index -= 1
@document.restore_from_memento(@history[@current_index])
true
end
def redo
return false if @current_index >= @history.length - 1
@current_index += 1
@document.restore_from_memento(@history[@current_index])
true
end
def can_undo?
@current_index > 0
end
def can_redo?
@current_index < @history.length - 1
end
end
# Usage
document = TextDocument.new
manager = UndoRedoManager.new(document)
manager.execute_command { document.insert_text("Hello") }
manager.execute_command { document.insert_text(" World") }
manager.execute_command { document.move_cursor(5) }
manager.execute_command { document.insert_text("!") }
puts document.content # => "Hello! World"
manager.undo
puts document.content # => "Hello World"
manager.undo
manager.undo
puts document.content # => "Hello"
manager.redo
puts document.content # => "Hello World"
The manager maintains branching history, discarding redo states when new operations occur after undo. Each command execution captures state before and after changes.
Game Save System with Multiple Slots implements persistent state storage:
class GameState
attr_reader :player_name, :current_level
def initialize(player_name)
@player_name = player_name
@current_level = 1
@player_stats = { health: 100, stamina: 100, gold: 0 }
@inventory = []
@completed_quests = []
@game_time = 0.0
end
def advance_level
@current_level += 1
end
def update_stats(stats)
@player_stats.merge!(stats)
end
def add_item(item)
@inventory << item
end
def complete_quest(quest_id)
@completed_quests << quest_id
end
def play_time(hours)
@game_time += hours
end
def create_save
GameSaveMemento.new(
player: @player_name,
level: @current_level,
stats: @player_stats.dup,
inventory: @inventory.dup,
quests: @completed_quests.dup,
time: @game_time
)
end
def load_save(save)
@player_name = save.player
@current_level = save.level
@player_stats = save.stats.dup
@inventory = save.inventory.dup
@completed_quests = save.quests.dup
@game_time = save.time
end
class GameSaveMemento
attr_reader :player, :level, :stats, :inventory, :quests, :time
def initialize(player:, level:, stats:, inventory:, quests:, time:)
@player = player
@level = level
@stats = stats.freeze
@inventory = inventory.freeze
@quests = quests.freeze
@time = time
@saved_at = Time.now
end
def display_info
{
player: @player,
level: @level,
progress: "#{@quests.length} quests completed",
playtime: "#{@time.round(1)} hours",
saved: @saved_at.strftime("%Y-%m-%d %H:%M")
}
end
def serialize
Marshal.dump(self)
end
def self.deserialize(data)
Marshal.load(data)
end
end
end
class SaveGameManager
def initialize
@save_slots = {}
end
def save_to_slot(slot_number, game_state)
@save_slots[slot_number] = game_state.create_save
end
def load_from_slot(slot_number, game_state)
save = @save_slots[slot_number]
return false unless save
game_state.load_save(save)
true
end
def list_saves
@save_slots.transform_values(&:display_info)
end
def export_save(slot_number, filename)
save = @save_slots[slot_number]
return false unless save
File.write(filename, save.serialize)
true
end
def import_save(slot_number, filename)
return false unless File.exist?(filename)
data = File.read(filename)
@save_slots[slot_number] = GameState::GameSaveMemento.deserialize(data)
true
end
end
# Usage
game = GameState.new("Player1")
manager = SaveGameManager.new
game.advance_level
game.update_stats(gold: 500)
game.add_item("Magic Sword")
game.complete_quest("quest_001")
game.play_time(2.5)
manager.save_to_slot(1, game)
game.advance_level
game.play_time(1.0)
manager.save_to_slot(2, game)
puts manager.list_saves
# Shows both saves with different progress
manager.load_from_slot(1, game)
puts game.current_level # => 2 (loaded earlier save)
The save system supports multiple slots, state serialization for persistence, and metadata display for slot selection. Each save captures complete game state independently.
Transactional Operations with Rollback shows memento usage in data operations:
class ShoppingCart
def initialize
@items = {} # product_id => { product: obj, quantity: int }
@discounts = []
@shipping_info = nil
end
def add_item(product, quantity)
if @items[product.id]
@items[product.id][:quantity] += quantity
else
@items[product.id] = { product: product, quantity: quantity }
end
end
def remove_item(product_id)
@items.delete(product_id)
end
def apply_discount(discount)
@discounts << discount
end
def set_shipping(info)
@shipping_info = info
end
def total
item_total = @items.values.sum do |item|
item[:product].price * item[:quantity]
end
discount_amount = @discounts.sum do |discount|
discount.calculate(item_total)
end
item_total - discount_amount
end
def begin_transaction
CartTransaction.new(self)
end
def create_checkpoint
CartMemento.new(
items: deep_copy(@items),
discounts: @discounts.dup,
shipping: @shipping_info&.dup
)
end
def restore_checkpoint(memento)
@items = deep_copy(memento.items)
@discounts = memento.discounts.dup
@shipping_info = memento.shipping&.dup
end
private
def deep_copy(obj)
Marshal.load(Marshal.dump(obj))
end
class CartMemento
attr_reader :items, :discounts, :shipping
def initialize(items:, discounts:, shipping:)
@items = items
@discounts = discounts
@shipping = shipping
@created_at = Time.now
end
end
end
class CartTransaction
def initialize(cart)
@cart = cart
@savepoint = cart.create_checkpoint
@committed = false
end
def commit
@committed = true
@savepoint = nil
end
def rollback
return if @committed
@cart.restore_checkpoint(@savepoint)
end
def execute
yield
commit
rescue StandardError => e
rollback
raise e
end
end
# Usage
Product = Struct.new(:id, :name, :price)
cart = ShoppingCart.new
cart.add_item(Product.new(1, "Book", 29.99), 1)
transaction = cart.begin_transaction
transaction.execute do
cart.add_item(Product.new(2, "Pen", 5.99), 2)
cart.apply_discount(Discount.new(10)) # 10% off
# If any operation raises exception, cart rolls back
raise "Payment failed" if some_condition
end
# Cart restored to pre-transaction state if exception raised
The transaction wraps cart modifications, automatically rolling back on failures. Each transaction creates an independent savepoint without affecting other operations.
Common Patterns
Incremental Memento reduces memory overhead by storing state deltas:
class DocumentEditor
def initialize
@full_state = { content: "", formatting: {}, metadata: {} }
@last_snapshot = @full_state.dup
end
def modify(changes)
@full_state.merge!(changes)
end
def create_incremental_memento
changes = compute_delta(@last_snapshot, @full_state)
if changes.size > @full_state.size * 0.5
# Delta too large, save full state
@last_snapshot = @full_state.dup
FullMemento.new(@full_state.dup)
else
IncrementalMemento.new(changes, @last_snapshot.object_id)
end
end
def restore(memento)
case memento
when FullMemento
@full_state = memento.state.dup
@last_snapshot = @full_state.dup
when IncrementalMemento
base_state = find_base_state(memento.base_id)
@full_state = apply_delta(base_state, memento.delta)
@last_snapshot = @full_state.dup
end
end
private
def compute_delta(old_state, new_state)
delta = {}
new_state.each do |key, value|
delta[key] = value if old_state[key] != value
end
delta
end
def apply_delta(base, delta)
base.merge(delta)
end
class FullMemento
attr_reader :state
def initialize(state)
@state = state
@id = object_id
end
end
class IncrementalMemento
attr_reader :delta, :base_id
def initialize(delta, base_id)
@delta = delta
@base_id = base_id
end
end
end
Incremental mementos store differences from a base state. When deltas grow large, the system reverts to full snapshots. This approach balances memory efficiency with restoration complexity.
Command Pattern Integration combines commands with mementos:
class Command
attr_reader :memento_before, :memento_after
def initialize(receiver)
@receiver = receiver
end
def execute
@memento_before = @receiver.create_memento
perform_action
@memento_after = @receiver.create_memento
end
def undo
@receiver.restore_memento(@memento_before) if @memento_before
end
def redo
@receiver.restore_memento(@memento_after) if @memento_after
end
def perform_action
raise NotImplementedError
end
end
class AddTextCommand < Command
def initialize(receiver, text, position)
super(receiver)
@text = text
@position = position
end
def perform_action
@receiver.insert(@text, @position)
end
end
class DeleteTextCommand < Command
def initialize(receiver, start_pos, length)
super(receiver)
@start_pos = start_pos
@length = length
end
def perform_action
@receiver.delete(@start_pos, @length)
end
end
class CommandManager
def initialize
@executed_commands = []
@current_index = -1
end
def execute(command)
command.execute
# Remove any commands after current index
@executed_commands = @executed_commands[0..@current_index]
@executed_commands << command
@current_index = @executed_commands.length - 1
end
def undo
return false if @current_index < 0
@executed_commands[@current_index].undo
@current_index -= 1
true
end
def redo
return false if @current_index >= @executed_commands.length - 1
@current_index += 1
@executed_commands[@current_index].redo
true
end
end
Commands store before/after mementos, enabling both undo (restore before-state) and redo (restore after-state) operations. This pattern works well when operations lack clean inverse functions.
Memento with Metadata attaches contextual information:
class AnnotatedMemento
attr_reader :state, :metadata
def initialize(state, metadata = {})
@state = state
@metadata = {
created_at: Time.now,
created_by: Thread.current[:user_id],
reason: nil,
tags: []
}.merge(metadata)
end
def tagged?(tag)
@metadata[:tags].include?(tag)
end
def age
Time.now - @metadata[:created_at]
end
def created_by?(user_id)
@metadata[:created_by] == user_id
end
end
class ConfigurationManager
def initialize
@config = {}
@snapshots = []
end
def update(key, value, reason: nil)
@config[key] = value
end
def create_snapshot(tags: [], reason: nil)
memento = AnnotatedMemento.new(
@config.dup,
tags: tags,
reason: reason
)
@snapshots << memento
memento
end
def find_snapshots(tag: nil, created_by: nil, max_age: nil)
@snapshots.select do |snapshot|
next false if tag && !snapshot.tagged?(tag)
next false if created_by && !snapshot.created_by?(created_by)
next false if max_age && snapshot.age > max_age
true
end
end
def restore_snapshot(memento)
@config = memento.state.dup
end
end
Metadata enables snapshot filtering, audit trails, and context-aware restoration. Applications can query snapshots by creator, age, tags, or custom attributes.
Copy-on-Write Memento shares immutable state:
class CopyOnWriteMemento
def initialize(data)
@data = data
@ref_count = 1
end
def share
@ref_count += 1
self
end
def release
@ref_count -= 1
@data = nil if @ref_count <= 0
end
def get_data
@data.dup
end
end
class StateManager
def initialize
@current_state = {}
@current_memento = nil
end
def modify(changes)
# First modification after snapshot creates new memento
if @current_memento
@current_memento.release
@current_memento = nil
end
@current_state.merge!(changes)
end
def snapshot
unless @current_memento
@current_memento = CopyOnWriteMemento.new(@current_state.dup)
end
@current_memento.share
end
end
Copy-on-write mementos share state until modifications occur. Reference counting tracks shared mementos, releasing memory when no references remain.
Common Pitfalls
Excessive Memory Consumption occurs when applications create too many mementos without pruning strategies. Each memento duplicates originator state, and unbounded history accumulation leads to memory exhaustion:
# Problematic: Unlimited history growth
class BadHistoryManager
def initialize(originator)
@originator = originator
@history = []
end
def save_state
@history << @originator.create_memento # Never removes old mementos
end
def restore(index)
@originator.restore_memento(@history[index])
end
end
# Solution: Implement history limits
class BoundedHistoryManager
def initialize(originator, max_size: 100)
@originator = originator
@history = []
@max_size = max_size
end
def save_state
@history << @originator.create_memento
# Remove oldest entries when limit exceeded
@history.shift if @history.size > @max_size
end
def clear_before(timestamp)
@history.reject! { |m| m.timestamp < timestamp }
end
end
Applications requiring long history should implement strategies like: keeping recent fine-grained snapshots with older coarse-grained snapshots, compressing old mementos, or archiving infrequently accessed states to disk.
Breaking Encapsulation Through Memento Access defeats the pattern's purpose:
# Problematic: External code accesses memento internals
class LeakyMemento
attr_accessor :data # Should be read-only or protected
def initialize(data)
@data = data
end
end
# External code manipulates memento
memento = originator.create_memento
memento.data[:field] = "modified" # Violates encapsulation
originator.restore_memento(memento) # Restores corrupted state
# Solution: Make mementos immutable and opaque
class SecureMemento
def initialize(data)
@data = data.freeze
end
# No public accessors
protected
attr_reader :data
end
Mementos should expose no internal state to external code. Only the originator class should access memento contents through protected or friend-class relationships.
Incorrect State Copying creates shared references instead of independent copies:
# Problematic: Shallow copy shares mutable references
class ShallowCopyProblem
def initialize
@data = { items: [] }
end
def add_item(item)
@data[:items] << item
end
def create_memento
BadMemento.new(@data) # Shares reference to @data
end
def restore(memento)
@data = memento.data # Now shares reference with memento
end
class BadMemento
attr_reader :data
def initialize(data)
@data = data # Reference, not copy
end
end
end
# Modifying restored state affects memento
obj = ShallowCopyProblem.new
obj.add_item("A")
memento = obj.create_memento
obj.add_item("B")
obj.restore(memento)
obj.add_item("C") # Modifies both obj and memento!
# Solution: Deep copy all mutable state
class ProperMemento
def initialize(data)
@data = Marshal.load(Marshal.dump(data))
end
def get_data
Marshal.load(Marshal.dump(@data))
end
end
Every memento operation requiring state transfer must create independent copies. Shallow copies suffice only when all state components are immutable.
Ignoring State Dependencies causes partial restoration:
# Problematic: Memento captures incomplete state
class PartialStateProblem
def initialize
@items = []
@index = 0
@filtered_view = [] # Derived from @items
end
def add(item)
@items << item
update_filtered_view
end
def create_memento
# Forgot to include @filtered_view
Memento.new(@items.dup, @index)
end
def restore(memento)
@items, @index = memento.items, memento.index
# @filtered_view now inconsistent!
end
private
def update_filtered_view
@filtered_view = @items.select { |i| i.active? }
end
end
# Solution: Capture all state or rebuild derived state
class CompleteStateCapture
def restore(memento)
@items = memento.items
@index = memento.index
update_filtered_view # Rebuild derived state
end
end
Mementos must capture either all state (including derived values) or ensure derived state gets rebuilt during restoration. Inconsistent state leads to subtle bugs.
Thread Safety Violations occur when concurrent operations access mementos or originators:
# Problematic: Race conditions during snapshot
class UnsafeSnapshot
def initialize
@data = []
@mutex = Mutex.new
end
def add(item)
@mutex.synchronize { @data << item }
end
def create_memento
# Reads @data without synchronization
Memento.new(@data.dup) # May see inconsistent state
end
end
# Solution: Synchronize snapshot operations
class ThreadSafeSnapshot
def create_memento
@mutex.synchronize do
Memento.new(@data.dup)
end
end
def restore(memento)
@mutex.synchronize do
@data = memento.data.dup
end
end
end
Concurrent access requires synchronization around both snapshot creation and restoration. Mementos themselves should be immutable, avoiding synchronization needs for memento storage.
Performance Impact from Frequent Snapshots degrades application responsiveness:
# Problematic: Snapshot on every character typed
class ExpensiveSnapshots
def initialize
@document = LargeDocument.new
end
def on_key_press(char)
snapshot = @document.create_memento # Expensive operation
@history.save(snapshot)
@document.insert(char)
end
end
# Solution: Batch snapshots or use timers
class EfficientSnapshots
def initialize
@document = LargeDocument.new
@pending_changes = false
@last_snapshot = Time.now
end
def on_key_press(char)
@document.insert(char)
@pending_changes = true
end
def periodic_snapshot
return unless @pending_changes
return if Time.now - @last_snapshot < 5 # 5 second minimum
@history.save(@document.create_memento)
@pending_changes = false
@last_snapshot = Time.now
end
end
Applications should balance snapshot frequency against performance impact. Strategies include batching changes, timer-based snapshots, or significance-based snapshots (capturing state only after substantial changes).
Reference
Core Components
| Component | Responsibility | Key Methods |
|---|---|---|
| Originator | Creates and restores from mementos | create_memento, restore_memento |
| Memento | Stores originator state snapshot | initialize, accessors (protected) |
| Caretaker | Manages memento storage/retrieval | save, undo, redo |
Implementation Patterns
| Pattern | When to Use | Trade-offs |
|---|---|---|
| Private nested class | Strong encapsulation needed | Ruby-specific, requires nesting |
| Module-based access | Shared memento behavior | More complex, flexible access control |
| Marshal serialization | Deep copying required | Performance cost, requires Marshal support |
| Struct-based | Simple immutable state | Lightweight, less flexible |
| Lazy capture | Large state spaces | Complex logic, potential inconsistency |
Memory Management Strategies
| Strategy | Description | Best For |
|---|---|---|
| Bounded history | Limit total mementos stored | Fixed memory footprint applications |
| Time-based pruning | Remove mementos older than threshold | Long-running applications |
| Significance filtering | Keep only important snapshots | Applications with many minor changes |
| Incremental mementos | Store state deltas | Frequently saved large states |
| Copy-on-write | Share immutable state | Multiple snapshots of unchanged state |
Access Control Approaches
| Approach | Mechanism | Encapsulation Level |
|---|---|---|
| Private constant | private_constant declaration | Strong, class-scoped |
| Protected methods | Ruby protected keyword | Medium, subclass access |
| Module inclusion | Shared module with protected methods | Medium, module-scoped |
| Friendship simulation | Conditional access checks | Custom, runtime checks |
Common State Copying Methods
| Method | Use Case | Considerations |
|---|---|---|
| dup | Shallow copy of simple objects | Shares nested object references |
| clone | Shallow copy preserving frozen state | Similar to dup, respects frozen |
| Marshal.dump/load | Deep copy of complex graphs | Performance impact, requires Marshal support |
| Custom deep copy | Fine-grained control | Manual implementation, error-prone |
| Frozen references | Immutable shared state | Requires immutability guarantee |
Memento Metadata Fields
| Field | Purpose | Example Value |
|---|---|---|
| timestamp | Creation time tracking | Time.now |
| creator_id | User attribution | current_user.id |
| reason | Change description | "Before bulk update" |
| tags | Categorization | [:checkpoint, :autosave] |
| byte_size | Memory tracking | serialized_data.bytesize |
| version | State version number | 42 |
Decision Matrix
| Requirement | Pattern Choice | Rationale |
|---|---|---|
| Simple undo/redo | Basic memento with stack | Straightforward history management |
| Branching history | Tree-structured caretaker | Supports multiple history paths |
| Minimal memory | Incremental mementos | Reduces storage overhead |
| Thread safety | Synchronized operations | Prevents race conditions |
| Serialization | Marshal-based mementos | Enables persistence |
| Rich queries | Annotated mementos | Supports history search |
Ruby-Specific Considerations
| Feature | Implementation | Notes |
|---|---|---|
| private_constant | Hide memento class | Prevents external instantiation |
| freeze | Make memento immutable | Prevents accidental modification |
| dup vs clone | Copy semantics | Choose based on frozen state needs |
| Marshal | Serialization | Standard library, deep copying |
| Struct | Lightweight mementos | Built-in value object |
| protected | Access control | Allows subclass/module access |
Performance Characteristics
| Operation | Complexity | Notes |
|---|---|---|
| Create memento | O(n) | n = size of state |
| Restore memento | O(n) | n = size of state |
| Store memento | O(1) | Assuming simple storage |
| Incremental create | O(m) | m = size of changes |
| Search history | O(h) | h = history size |
Integration Patterns
| Pattern | Description | Use Case |
|---|---|---|
| Command + Memento | Commands store before/after states | Complex undo/redo |
| Strategy + Memento | Different save strategies | Adaptive optimization |
| Observer + Memento | Notify on state capture | Audit logging |
| Prototype + Memento | Clone with state | Object replication |