Skip to content

Memento

The Memento pattern describes how to manage the saved states of an object. Imagine a game (e.g. running on a console) that has auto-save functionality in certain places. In this game, you move your character around the world. When such a save occurs, the game "must know", for example, what equipment your character currently has, which tasks in the game have been completed and which are ongoing, what places have been visited , to correctly display the world map.

When you enter the options menu, you can see that there is an option to "Load previous checkpoint". After choosing this option you are moved to the previous save location. Suppose that at this point, you can select the same option again and be transferred to an even earlier checkpoint.

Construction

How are saved game objects stored? Well, they can be stored anywhere, depending on needed functionality (database, separate files, RAM), but they must keep a copy of a certain state. Of course, it must be possible to create such a copy of the state, and later it must be possible to restore the state.

All the elements described above compose the implementation of the Memento pattern, i.e. we need:

  • an object representing the state of the application (or the state of a certain part)
  • an object representing the saved state of the application
  • the possibility to create a save object from the original state
  • the ability to restore the original state based on the saved state
  • an object that somehow manages the saved states.

ATTENTION: When copying states, most often we have to use the deep copy. This mechanism was discussed in the description of the prototype.

Example

The example shows how to save a certain game. This state is kept in the GameState class, while the GameStateSnapshot class represents its save. The GameStateSnapshot instance is created by creating a copy of the GameState class fields. There is also an option to restore status of the GameState class, based on the value in the GameStateSnapshot instance (i.e. between the objects GameState and GameStateSnapshot it is possible to make a deep copy in both directions).

The object that manages the saved states is the GameStateManager class, which allows you to save and load the game only from the previous state. A stack was used for this purpose. One of the built-in collections that can be used is the ArrayDeque class, which implements the Deque interface:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class GameState {
  private Integer health;
  private Integer mana;
  private List<String> items;

  @Override
  public String toString() {
    return "GameState{" +
        "health=" + health +
        ", mana=" + mana +
        ", items=" + items +
        "}\n";
  }

  public void heal() {
    health = 100;
  }

  public void takeDamage(final int damage) {
    health -= damage;
  }

  public void addItem(final String item) {
    items.add(item);
  }

  public void loseAllItems() {
    items.clear();
  }

  public void restoreFromSnapshot(final GameStateSnapshot snapshot) {
    health = snapshot.getHealth();
    mana = snapshot.getMana();
    items = List.copyOf(snapshot.getItems());
  }
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class GameStateSnapshot {

  private Integer health;
  private Integer mana;
  private List<String> items;

  public GameStateSnapshot(final GameState gameState) {
    this.health = gameState.getHealth();
    this.mana = gameState.getMana();
    this.items = List.copyOf(gameState.getItems()); // deep copy!
  }
}
import java.util.ArrayDeque;
import java.util.Deque;

public class GameStateManager {

  private final Deque<GameStateSnapshot> snapshots = new ArrayDeque<>();

  public void saveGame(final GameState gameState) {
    snapshots.push(new GameStateSnapshot(gameState));
  }

  public GameStateSnapshot restorePreviousCheckpoint() {
    return snapshots.pop();
  }
}
import java.util.ArrayList;

public class MementoUsage {
  public static void main(String[] args) {
    final GameState gameState = new GameState(100, 80, new ArrayList<>());

    final GameStateManager gameStateManager = new GameStateManager();
    gameStateManager.saveGame(gameState);
    System.out.println(gameState);

    gameState.addItem("Basic Sword");
    gameState.takeDamage(10);
    System.out.println(gameState);

    gameStateManager.saveGame(gameState);

    gameState.takeDamage(50);
    gameState.addItem("Shield");
    System.out.println(gameState);

    gameStateManager.saveGame(gameState);

    // decided to load previous save twice

    gameStateManager.restorePreviousCheckpoint();
    final GameStateSnapshot gameStateSnapshot = gameStateManager.restorePreviousCheckpoint();
    gameState.restoreFromSnapshot(gameStateSnapshot);
    System.out.println(gameState);
  }
}

/* OUTPUT:
GameState{health=100, mana=80, items=[]}

GameState{health=90, mana=80, items=[Basic Sword]}

GameState{health=40, mana=80, items=[Basic Sword, Shield]}

GameState{health=90, mana=80, items=[Basic Sword]}
*/

ATTENTION: Storing many saved states in memory may result in a significant increase in memory consumption. In this case it is worth considering additional use of the Flyweight pattern.