189
behavioral memory management

Snapshot

Reference Wikipedia ↗
Snapshot — class diagram
Plate 189 class diagram

The Snapshot pattern captures the internal state of an object at a particular point in time, allowing it to be restored to that state later. This is achieved by creating a “snapshot” or “memento” of the object’s state, which is then stored by a separate “caretaker” object. The originator can then recreate itself from the snapshot if required.

This pattern is particularly useful when implementing features like undo/redo functionality, transaction management, or version control. It allows for state recovery without violating encapsulation, as the snapshot holds the internal state without exposing it directly to the caretaker. This isolation preserves the originator’s control over its data.

Usage

The Snapshot pattern is widely used in scenarios requiring state persistence and recovery:

  • Undo/Redo functionality: Text editors, image manipulation software, and game engines use this pattern to enable users to undo or redo actions. Each action creates a snapshot of the application’s state before the action is applied.
  • Transaction Management: Databases and financial systems utilize snapshots to ensure that transactions can be rolled back to a consistent state in case of failure.
  • Version Control Systems: The core concept behind version control systems like Git is to maintain snapshots of files and directories over time, allowing users to revert to previous versions.
  • Game Saving: Games often use snapshots to store the player’s progress, including the game world state, player statistics, and inventory.

Examples

  • Git: Git fundamentally relies on the Snapshot pattern. Every commit represents a snapshot of the entire project’s state at that moment. Git efficiently stores these snapshots by only saving the differences between versions. The .git directory acts as the caretaker, holding the history of snapshots.

  • Redux (JavaScript Library): Redux uses a single immutable state tree. Actions trigger state changes, and a reducer function calculates the new state. Before each action, the current state is effectively a snapshot. The store maintains the history of these snapshots, allowing for time-travel debugging and implementing undo/redo features. The combineReducers function and middleware like Redux Thunk leverage this snapshotting capability.

  • Word/Google Docs: These applications use snapshots to automatically save documents at intervals, and to facilitate the undo/redo functionality. The application (Originator) creates a snapshot of the document’s state, and a background service (Caretaker) stores these snapshots.

Specimens

15 implementations
Specimen 189.01 Dart View specimen ↗

The Snapshot pattern captures and restores the state of an object or system, allowing for rollbacks or efficient re-initialization. This is achieved by serializing the object’s state to a persistent storage (like a file) and then deserializing it to recreate that state. In Dart, this is naturally implemented using the encode and decode methods from the dart:convert library, often with JSON as the serialization format. The example demonstrates saving and restoring a simple Counter object’s value. Using JSON is idiomatic Dart for data serialization due to its readability and ease of use with Dart’s built-in data structures.

import 'dart:convert';
import 'dart:io';

class Counter {
  int value;

  Counter(this.value);

  @override
  String toString() => 'Counter(value: $value)';

  Map<String, dynamic> toJson() => {'value': value};

  factory Counter.fromJson(Map<String, dynamic> json) = Counter._;

  Counter._(this.value);
}

Future<void> saveSnapshot(Counter counter, String filePath) async {
  final json = counter.toJson();
  final file = File(filePath);
  await file.writeAsString(jsonEncode(json));
  print('Snapshot saved to $filePath');
}

Future<Counter> loadSnapshot(String filePath) async {
  final file = File(filePath);
  final contents = await file.readAsString();
  final json = jsonDecode(contents) as Map<String, dynamic>;
  final counter = Counter.fromJson(json);
  print('Snapshot loaded from $filePath');
  return counter;
}

void main() async {
  var counter = Counter(10);
  print('Initial counter: $counter');

  const filePath = 'counter_snapshot.json';
  await saveSnapshot(counter, filePath);

  counter = Counter(0); // Reset counter
  print('Counter reset: $counter');

  counter = await loadSnapshot(filePath);
  print('Counter restored: $counter');
}