As developers, we’re all too familiar with hitting Ctrl + Z
to undo our code changes. It’s almost second nature at this point. But have you ever stopped to wonder what really happens when you hit that magic combination? Sure, we might think, ‘It just stores the previous state somewhere and then brings it back,’ but that’s only part of the story.
The real magic behind undoing your changes lies in the Memento Design Pattern. This pattern is what makes undo functionality so efficient and seamless. Curious to know how it works? Let’s dive in and take a closer look!
Memento Design Pattern
The Memento Design Pattern is one of the behavioral design patterns. It’s all about capturing an object’s state at a particular moment in time, so you can restore it later. It’s like taking a snapshot of an object’s current state and saving it for safekeeping.
Here’s the official definition:
The Memento Design Pattern provides the ability to restore an object to its previous state without exposing the implementation details.
We can’t always expose an object’s internal details due to encapsulation. This pattern allows us to:
- Save the state without breaking encapsulation.
- Support features like undo/redo, checkpoints, or versioning.
Real-Life Analogy
Imagine writing a letter and using an eraser. Before making changes, you take a photo of the letter. If you mess up, you can refer to the photo and restore the original version. In this analogy:
- The letter is the Originator.
- The photo is the Memento.
- You, with your eraser, are the Caretaker.
Memento Design Pattern Structure
Encapsulate an object’s state so that its internal structure is hidden from external entities. The core of the structure lies in the memento, which stores the object’s state, while another object, known as the caretaker, is responsible for saving and restoring the memento without accessing the object’s internal details.
The Memento pattern involves three primary components:
Memento: It’s like a snapshot of an object’s internal state. It saves the object’s data at a specific moment, and it can save only what is necessary.
- Protection: The Memento ensures that only the object that created it (the “Originator”) can access and modify its content. Other objects (like the “Caretaker”) can only store and pass the Memento around without seeing or changing its data.
Originator: This is the object that wants to save its state.
- It creates a Memento to store its current state.
- Later, it can use the Memento to go back to that saved state.
Caretaker: This object is responsible for keeping the Memento safe.
- It never looks inside the Memento or changes its content. It simply stores and retrieves it when needed.
In short, the Originator creates a snapshot (Memento) of its state, and the Caretaker keeps track of these snapshots. Later, the Originator can use the snapshots to restore itself.
In practice, the Originator creates a Memento to store its state, and the Caretaker keeps the Memento for future use. When the user triggers an undo (like pressing Ctrl + Z
), the Caretaker retrieves the saved state from the Memento and hands it back to the Originator, which then reverts to that state.
// Memento class stores the state of the Originator
class Memento(val state: String)
// Originator is the object whose state is saved
class Originator(var state: String) {
// Creates a Memento with the current state
fun createMemento(): Memento {
return Memento(state)
}
// Restores the state from a Memento
fun restore(memento: Memento) {
this.state = memento.state
}
fun showState() {
println("Current State: $state")
}
}
// Caretaker is responsible for storing and restoring the Memento
class Caretaker {
private val mementoList = mutableListOf<Memento>()
// Adds a Memento to the list
fun addMemento(memento: Memento) {
mementoList.add(memento)
}
// Retrieves the last saved Memento (undo functionality)
fun getLastMemento(): Memento? {
if (mementoList.isNotEmpty()) {
return mementoList.removeAt(mementoList.size - 1)
}
return null
}
}
// Demonstrating the Memento pattern in action
fun main() {
val originator = Originator("Initial State")
val caretaker = Caretaker()
originator.showState() // Output: Current State: Initial State
// Save the state
caretaker.addMemento(originator.createMemento())
// Change the state
originator.state = "State 1"
originator.showState() // Output: Current State: State 1
// Save the new state
caretaker.addMemento(originator.createMemento())
// Change the state again
originator.state = "State 2"
originator.showState() // Output: Current State: State 2
// Now let's undo the last state change (Ctrl + Z)
val lastMemento = caretaker.getLastMemento()
if (lastMemento != null) {
originator.restore(lastMemento)
originator.showState() // Output: Current State: State 1
}
// Undo again (Ctrl + Z)
val previousMemento = caretaker.getLastMemento()
if (previousMemento != null) {
originator.restore(previousMemento)
originator.showState() // Output: Current State: Initial State
}
}
Output
Current State: Initial State
Current State: State 1
Current State: State 2
Current State: State 1
Current State: Initial State
Here,
- Initially, the
Originator
has the state “Initial State”. - The state is saved into the
Caretaker
‘s list. - The state changes to “State 1”, and it is saved again.
- The state changes to “State 2”, and the change is saved.
- The
Caretaker
then provides the last saved state (from “State 2” to “State 1”), and then the previous state (“State 1” to “Initial State”) is restored.
A few key points remain to be highlighted regarding how the Memento pattern works and its structure, particularly focusing on the concepts of a wide interface and a narrow interface.
What is a Wide Interface and a Narrow Interface?
In the context of the Memento Design Pattern, the terms wide interface and narrow interface typically refer to the scope of access provided by the Memento class.
Wide Interface: A Memento with a wide interface exposes more details of the internal state of the Originator. This allows the Caretaker to access and potentially modify the internal data of the Memento. This approach can lead to more flexibility but might break encapsulation.
Narrow Interface: A Memento with a narrow interface provides limited access to the internal state of the Originator. It hides the details of the state, ensuring that only essential information is made available. This helps maintain encapsulation and prevents external manipulation of the internal state, ensuring that the Originator remains in control.
In short,
- Wide interface gives more access to the internal state.
- Narrow interface restricts access to only necessary information, maintaining better encapsulation and control.
Real Scenario: A Text Editor with Undo Functionality
Let’s implement a simple text editor with undo functionality. We’ll use the Memento pattern to save and restore the editor’s state.
Define the Originator
The Originator is the class whose state we want to save and restore. In our case, it’s the TextEditor
class.
// Originator: TextEditor
class TextEditor {
var content: String = ""
// Save current state as a Memento
fun save(): Memento {
return Memento(content)
}
// Restore state from a Memento
fun restore(memento: Memento) {
content = memento.state
}
// Nested Memento class to encapsulate the state
data class Memento(val state: String)
}
Here’s what’s happening:
- The
content
variable represents the editor’s current text. - The
save()
method creates aMemento
containing the current state. - The
restore()
method updates the editor’s state using aMemento
. - One more twist is that we used a Nested Memento, but it doesn’t really affect the approach. So, go ahead with this approach as well.
Create the Caretaker
The Caretaker manages the mementos. It decides when to save and restore the state. For simplicity, we’ll use a stack (List) to store multiple mementos (for undo functionality).
// Caretaker: Manages mementos
class Caretaker {
private val mementoStack = mutableListOf<TextEditor.Memento>()
// Save a memento
fun save(memento: TextEditor.Memento) {
mementoStack.add(memento)
}
// Retrieve the last memento
fun undo(): TextEditor.Memento? {
if (mementoStack.isNotEmpty()) {
return mementoStack.removeAt(mementoStack.size - 1)
}
return null
}
}
Here,
- The
save()
method pushes a memento onto the stack. - The
undo()
method pops the last memento, providing the most recent state.
Tie Everything Together
Now let’s combine the Originator and Caretaker to see the Memento pattern in action.
fun main() {
val textEditor = TextEditor()
val caretaker = Caretaker()
// Initial content
textEditor.content = "Hello, World!"
println("Content: ${textEditor.content}")
// Save state
caretaker.save(textEditor.save())
// Modify content
textEditor.content = "Hello, Kotlin!"
println("Modified Content: ${textEditor.content}")
// Save another state
caretaker.save(textEditor.save())
// Modify content again
textEditor.content = "Design Patterns are fun!"
println("Further Modified Content: ${textEditor.content}")
// Undo last change
val lastState = caretaker.undo()
if (lastState != null) {
textEditor.restore(lastState)
println("After Undo: ${textEditor.content}")
}
// Undo again
val previousState = caretaker.undo()
if (previousState != null) {
textEditor.restore(previousState)
println("After Another Undo: ${textEditor.content}")
}
}
Output
Content: Hello, World!
Modified Content: Hello, Kotlin!
Further Modified Content: Design Patterns are fun!
After Undo: Hello, Kotlin!
After Another Undo: Hello, World!
Furthermore, we can apply this pattern to similar use cases, such as:
- Games: Saving checkpoints or progress.
- Graphical Editors: Reverting to earlier canvas states.
Benefits of the Memento Pattern
- Encapsulation: The
Memento
class keeps the state private, adhering to encapsulation. - Undo/Redo: It’s perfect for features like undo/redo in editors or games.
- Simple and Scalable: Easy to implement and extend for more complex states.
Limitations of the Memento Pattern
- Memory Usage: Storing many mementos can consume a lot of memory.
- Complexity for Large States: If the state is large or includes references to other objects, managing mementos can get tricky.
Tips for Using the Memento Pattern in Kotlin
- Immutable Mementos: Ensure mementos are immutable to avoid accidental changes.
- Use Serialization for Complex States: For large or nested states, consider saving mementos using Kotlin serialization.
- Limit History: To save memory, consider capping the number of mementos stored.
Conclusion
The Memento design pattern is a powerful tool for managing an object’s state in a controlled and encapsulated way. In this blog, we implemented the pattern in Kotlin, showcasing its practical usage with a simple Text Editor example.
By mastering the Memento pattern, you can build applications that support undo/redo features or maintain state history without violating encapsulation principles. As seen in Kotlin, the pattern is intuitive and aligns well with the language’s concise syntax and data class features.
Feel free to experiment with the pattern in your projects. Whether it’s a game, an editor, or a stateful application, the Memento pattern will prove invaluable!