Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
294: The Memento Pattern
We've all been there: you're building a feature, and suddenly the stakeholder asks for an "Undo" button. At first glance, it seems trivial. You just need to save the state of the object before you change it, right? But as your objects grow in complexity, the way you implement that "save" can either keep your code clean or turn your architecture into a tangled mess of public getters and setters.
The temptation to just expose everything
The most intuitive—and usually wrong—way to handle this is to make the object's internal state public enough that some external "History Manager" can read it. Imagine we're building a RPG game where a GameCharacter has health, mana, and coordinates. You might be tempted to do something like this:
public class GameCharacter {
private int health;
private int mana;
private int x, y;
// Getters for everything so the SaveSystem can see them
public int getHealth() { return health; }
public int getMana() { return mana; }
public int getX() { return x; }
public int getY() { return y; }
public void setHealth(int health) { this.health = health; }
// ... other setters
}
In this naive approach, your SaveSystem class would simply call all those getters, store the values in a Map or a POJO, and then call the setters to restore them. It works. For about a week. Then you add a new attribute—maybe stamina or experiencePoints—and suddenly your SaveSystem is broken. You have to go back and manually update the save logic every single time the GameCharacter evolves.
Why public getters are a trap for state management
The problem here isn't just the maintenance headache; it's a fundamental violation of encapsulation. By exposing the internal state just to support an undo feature, you're telling the rest of your application that it's okay to reach inside the GameCharacter and mess with its guts. You've effectively made your private fields public, just with more keystrokes. I've seen this lead to bugs where other developers start using those "save-only" getters to implement game logic, creating a spiderweb of dependencies that makes the code impossible to refactor.
Hiding the state in a Memento
The Memento pattern solves this by shifting the responsibility of "remembering" back to the object that actually owns the data. Instead of the SaveSystem pulling data out, the GameCharacter pushes a snapshot of itself into a small, opaque object called a Memento.
The trick is to make the Memento's internal state inaccessible to everyone except the character who created it. In Java, we can do this beautifully with a private inner class:
public class GameCharacter {
private int health;
private int mana;
// The Memento is a snapshot of the state
public static class Memento {
private final int health;
private final int mana;
private Memento(int health, int mana) {
this.health = health;
this.mana = mana;
}
private int getHealth() { return health; }
private int getMana() { return mana; }
}
public Memento save() {
return new Memento(health, mana);
}
public void restore(Memento memento) {
this.health = memento.getHealth();
this.mana = memento.getMana();
}
}
Now, look at what happened to the SaveSystem. It no longer needs to know that GameCharacter has health or mana. It just holds a reference to a Memento object. It's essentially holding a sealed envelope; it knows the envelope contains the state, but it can't open it or change what's inside. It just hands the envelope back to the character when it's time to restore.
Trading memory for encapsulation
Is this approach perfect? No. There's a trade-off. You're now creating new objects every time you take a snapshot. If your state is massive—think thousands of entities in a game world—creating Mementos every few frames will spike your garbage collection and eat through your heap. In those cases, you might need to look at more complex delta-based saves (saving only what changed).
But for the vast majority of business logic, the trade-off is a no-brainer. You get a clean separation of concerns: the GameCharacter manages its own data, and the SaveSystem (the "Caretaker") simply manages the timeline of snapshots. You can add ten new fields to your character, and as long as you update the inner Memento class, your save/undo logic remains completely untouched.
📋 Practical Task
Implement a Snapshot System for a Text Editor Buffer
You are building a simplified text editor. Create a TextBuffer class that maintains a StringBuilder for the current text and an int for the cursor position. Implement the Memento pattern to allow the buffer to be restored to a previous state.
- Create a private inner
Mementoclass withinTextBufferthat captures both the text content and the cursor position. - Implement a
save()method that returns aMemento. - Implement a
restore(Memento memento)method that reverts the buffer and cursor. - Create a
Historyclass (the Caretaker) that uses aStackto store these mementos, providingpush(Memento m)andpop()methods.
Test your implementation: Write a main method where you add text, save a snapshot, add more text, move the cursor, and then successfully restore the buffer to the exact state it was in at the time of the snapshot.
There are no comments for now.