Memento
Also known as¶
- Snapshot
- Token
Intent¶
Capture and externalize an object's internal state so that it can be restored later, without violating encapsulation.
Explanation¶
Real-world example
A text editor captures the current state of a document as a memento each time a change is made. The snapshots are stored in a history list. When the user clicks undo, the editor restores the document to the state saved in the most recent memento, without exposing or altering the internal data structures.
In plain words
The Memento pattern captures an object's internal state so it can be stored and restored at any point in time.
Wikipedia says
The memento pattern is a software design pattern that provides the ability to restore an object to its previous state (undo via rollback).
Sequence diagram
sequenceDiagram
participant Caretaker
participant Star
participant StarMemento
Caretaker->>Star: star.memento (get)
Star-->>StarMemento: create(type, age, mass)
StarMemento-->>Caretaker: memento
Caretaker->>Star: timePasses()
Caretaker->>Star: star.memento = saved (set)
Star->>StarMemento: read state
Star-->>Star: restore type, age, mass
Programmatic Example¶
First we define the lifecycle stages of a star.
internal enum class StarType(private val title: String) {
DEAD("dead star"),
RED_GIANT("red giant"),
SUN("sun"),
SUPERNOVA("supernova"),
WHITE_DWARF("white dwarf"),
;
override fun toString(): String = title
}
The [StarMemento] interface is an opaque handle that hides the actual state from the caretaker.
The [Star] class is the originator. It stores its state in a
private data class that implements [StarMemento], so outside
code cannot inspect or modify the saved state.
internal class Star(
private var type: StarType,
private var ageYears: Int,
private var massTons: Int,
) {
fun timePasses() {
ageYears *= 2
massTons *= 8
when (type) {
StarType.SUN -> type = StarType.RED_GIANT
StarType.RED_GIANT -> type = StarType.WHITE_DWARF
StarType.WHITE_DWARF -> type = StarType.SUPERNOVA
StarType.SUPERNOVA -> type = StarType.DEAD
StarType.DEAD -> {
ageYears *= 2
massTons = 0
}
}
}
var memento: StarMemento
get() = StarSnapshot(
type = type,
ageYears = ageYears,
massTons = massTons,
)
set(value) {
val state = value as StarSnapshot
type = state.type
ageYears = state.ageYears
massTons = state.massTons
}
override fun toString(): String =
"$type age: $ageYears years mass: $massTons tons"
private data class StarSnapshot(
val type: StarType,
val ageYears: Int,
val massTons: Int,
) : StarMemento
}
Here is how the caretaker uses a stack of mementos to save and restore star states.
fun main() {
val states = ArrayDeque<StarMemento>()
val star = Star(StarType.SUN, 10_000_000, 500_000)
logger.info(star.toString())
states.push(star.memento)
star.timePasses()
logger.info(star.toString())
states.push(star.memento)
star.timePasses()
logger.info(star.toString())
states.push(star.memento)
star.timePasses()
logger.info(star.toString())
states.push(star.memento)
star.timePasses()
logger.info(star.toString())
while (states.isNotEmpty()) {
star.memento = states.pop()
logger.info(star.toString())
}
}
Program output:
sun age: 10000000 years mass: 500000 tons
red giant age: 20000000 years mass: 4000000 tons
white dwarf age: 40000000 years mass: 32000000 tons
supernova age: 80000000 years mass: 256000000 tons
dead star age: 160000000 years mass: 2048000000 tons
supernova age: 80000000 years mass: 256000000 tons
white dwarf age: 40000000 years mass: 32000000 tons
red giant age: 20000000 years mass: 4000000 tons
sun age: 10000000 years mass: 500000 tons
Class diagram¶
classDiagram
class StarMemento {
<<interface>>
}
class Star {
-type: StarType
-ageYears: Int
-massTons: Int
+timePasses()
+memento: StarMemento
+toString() String
}
class StarSnapshot {
<<data class>>
+type: StarType
+ageYears: Int
+massTons: Int
}
class StarType {
<<enum>>
DEAD
RED_GIANT
SUN
SUPERNOVA
WHITE_DWARF
+toString() String
}
Star ..> StarMemento : creates
Star --> StarType
StarSnapshot ..|> StarMemento
StarSnapshot --o Star : inner class
StarSnapshot --> StarType
Applicability¶
Use the Memento pattern when:
- You need to capture an object's state and restore it later without exposing its internal structure
- A direct interface to obtaining the state would expose implementation details and break encapsulation
Consequences¶
Benefits:
- Preserves encapsulation boundaries
- Simplifies the originator by removing the need to manage version history or undo functionality directly
Trade-offs:
- Can be expensive in terms of memory if a large number of states are saved
- Care must be taken to manage the lifecycle of mementos to avoid memory leaks