Memento Pattern
The Memento pattern provides a way to capture and externalize an object’s internal state so that the object can be restored to this state later without violating encapsulation. It’s often used to implement undo/redo functionality in applications. This pattern involves three participants: the Originator (the object whose state needs to be saved), the Memento (an object that stores the Originator’s state), and the Caretaker (an object that manages the Mementos).
What is Memento Pattern
The Memento pattern addresses the problem of preserving an object’s internal state while maintaining encapsulation. Directly exposing the object’s state would violate encapsulation, making the object vulnerable to external modifications. The Memento pattern solves this by creating a separate object, the Memento, which encapsulates the internal state. The Originator is responsible for creating and restoring Mementos. The Caretaker is responsible for holding and managing Mementos, but it never operates on or examines the contents of a Memento.
In-depth explanation:
The core idea is to decouple the state preservation mechanism from the Originator’s core logic. The Originator retains control over what state is saved and how it’s restored. The Memento acts as an immutable snapshot of the Originator’s state at a specific point in time. The Caretaker provides a history mechanism, allowing the application to revert to previous states.
Edge Cases:
- Large State: If the Originator’s state is very large, creating and storing Mementos can become expensive in terms of memory and performance. Consider saving only the delta (changes) between states instead of the entire state.
- Complex State: If the Originator’s state involves complex data structures or dependencies, the Memento might need to be carefully designed to ensure that all necessary information is captured and restored correctly. Deep copying might be required.
- Security: If the Originator’s state contains sensitive information, the Memento needs to be protected from unauthorized access. Consider encrypting the Memento’s contents.
Performance Considerations:
- Memory Usage: The number of Mementos stored by the Caretaker directly impacts memory usage. Implement a strategy to limit the number of Mementos stored, such as discarding older Mementos or using a circular buffer.
- Copying Overhead: Creating Mementos involves copying the Originator’s state. The cost of copying can be significant for large or complex states. Consider using techniques like copy-on-write to reduce the copying overhead.
- Serialization/Deserialization: If Mementos need to be persisted to disk or transmitted over a network, serialization and deserialization can add overhead. Choose an efficient serialization format and consider using compression to reduce the size of the serialized data.
Syntax and Usage
The Memento pattern typically involves the following classes:
- Originator: The object whose state is to be saved. It creates Mementos containing its current state and can restore its state from a Memento.
- Memento: An object that stores the internal state of the Originator. It is essentially a black box to the Caretaker.
- Caretaker: Manages the Mementos. It doesn’t know the internal structure of the Memento.
// Memento Interface (Abstract)
class Memento {
public:
virtual ~Memento() = default;
virtual std::string getState() const = 0; // Common interface to retrieve state
};
// Concrete Memento
class ConcreteMemento : public Memento {
private:
std::string state_;
public:
ConcreteMemento(const std::string& state) : state_(state) {}
std::string getState() const override {
return state_;
}
};
// Originator
class Originator {
private:
std::string state_;
public:
void setState(const std::string& state) {
std::cout << "Originator: Setting state to " << state << std::endl;
state_ = state;
}
std::string getState() const {
return state_;
}
Memento* saveStateToMemento() {
std::cout << "Originator: Saving to Memento." << std::endl;
return new ConcreteMemento(state_);
}
void restoreStateFromMemento(Memento* memento) {
ConcreteMemento* concreteMemento = dynamic_cast<ConcreteMemento*>(memento);
if (concreteMemento) {
state_ = concreteMemento->getState();
std::cout << "Originator: State after restoring from Memento: " << state_ << std::endl;
} else {
std::cerr << "Error: Invalid Memento type." << std::endl;
}
}
};
// Caretaker
class Caretaker {
private:
std::vector<Memento*> mementos_;
Originator* originator_;
public:
Caretaker(Originator* originator) : originator_(originator) {}
void addMemento(Memento* memento) {
std::cout << "Caretaker: Adding Memento." << std::endl;
mementos_.push_back(memento);
}
Memento* getMemento(int index) {
if (index >= 0 && index < mementos_.size()) {
return mementos_[index];
}
std::cerr << "Error: Invalid Memento index." << std::endl;
return nullptr;
}
~Caretaker() {
for (Memento* memento : mementos_) {
delete memento;
}
}
};
Basic Example
#include <iostream>
#include <vector>
#include <string>
// (Classes from Syntax and Usage section go here)
int main() {
Originator originator;
Caretaker caretaker(&originator);
originator.setState("State #1");
caretaker.addMemento(originator.saveStateToMemento());
originator.setState("State #2");
caretaker.addMemento(originator.saveStateToMemento());
originator.setState("State #3");
std::cout << "Current State: " << originator.getState() << std::endl;
std::cout << "Undoing to State #2..." << std::endl;
originator.restoreStateFromMemento(caretaker.getMemento(1));
std::cout << "Current State: " << originator.getState() << std::endl;
std::cout << "Undoing to State #1..." << std::endl;
originator.restoreStateFromMemento(caretaker.getMemento(0));
std::cout << "Current State: " << originator.getState() << std::endl;
return 0;
}Explanation:
- An
Originatorobject is created. - A
Caretakerobject is created, associated with theOriginator. - The
Originator’s state is set to “State #1”, and a Memento is created and added to theCaretaker. - The
Originator’s state is set to “State #2”, and another Memento is created and added to theCaretaker. - The
Originator’s state is set to “State #3”. - The
restoreStateFromMementomethod is called on theOriginator, using Mementos retrieved from theCaretaker, effectively undoing the state changes.
Advanced Example
This example demonstrates a more complex scenario where the Originator has multiple state variables, and the Memento needs to capture all of them. We also implement a limit to the number of mementos stored to avoid excessive memory usage.
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
// Memento Interface (Abstract)
class Memento {
public:
virtual ~Memento() = default;
virtual std::string getState() const = 0; // Common interface to retrieve state
virtual int getVersion() const = 0;
};
// Concrete Memento
class ConcreteMemento : public Memento {
private:
std::string state_;
int version_;
public:
ConcreteMemento(const std::string& state, int version) : state_(state), version_(version) {}
std::string getState() const override {
return state_;
}
int getVersion() const override {
return version_;
}
};
// Originator
class Originator {
private:
std::string state_;
int version_;
public:
void setState(const std::string& state) {
std::cout << "Originator: Setting state to " << state << std::endl;
state_ = state;
}
void setVersion(int version) {
std::cout << "Originator: Setting version to " << version << std::endl;
version_ = version;
}
std::string getState() const {
return state_;
}
int getVersion() const {
return version_;
}
Memento* saveStateToMemento() {
std::cout << "Originator: Saving to Memento." << std::endl;
return new ConcreteMemento(state_, version_);
}
void restoreStateFromMemento(Memento* memento) {
ConcreteMemento* concreteMemento = dynamic_cast<ConcreteMemento*>(memento);
if (concreteMemento) {
state_ = concreteMemento->getState();
version_ = concreteMemento->getVersion();
std::cout << "Originator: State after restoring from Memento: " << state_ << ", Version: " << version_ << std::endl;
} else {
std::cerr << "Error: Invalid Memento type." << std::endl;
}
}
};
// Caretaker
class Caretaker {
private:
std::vector<Memento*> mementos_;
Originator* originator_;
const size_t maxMementos_;
public:
Caretaker(Originator* originator, size_t maxMementos = 5) : originator_(originator), maxMementos_(maxMementos) {}
void addMemento(Memento* memento) {
std::cout << "Caretaker: Adding Memento." << std::endl;
mementos_.push_back(memento);
if (mementos_.size() > maxMementos_) {
std::cout << "Caretaker: Reached maximum mementos. Removing oldest." << std::endl;
delete mementos_.front();
mementos_.erase(mementos_.begin());
}
}
Memento* getMemento(int index) {
if (index >= 0 && index < mementos_.size()) {
return mementos_[index];
}
std::cerr << "Error: Invalid Memento index." << std::endl;
return nullptr;
}
~Caretaker() {
for (Memento* memento : mementos_) {
delete memento;
}
}
};
int main() {
Originator originator;
Caretaker caretaker(&originator, 3); // Limit to 3 mementos
originator.setState("State #1");
originator.setVersion(1);
caretaker.addMemento(originator.saveStateToMemento());
originator.setState("State #2");
originator.setVersion(2);
caretaker.addMemento(originator.saveStateToMemento());
originator.setState("State #3");
originator.setVersion(3);
caretaker.addMemento(originator.saveStateToMemento());
originator.setState("State #4");
originator.setVersion(4);
caretaker.addMemento(originator.saveStateToMemento()); // Will remove State #1
originator.setState("State #5");
originator.setVersion(5);
caretaker.addMemento(originator.saveStateToMemento()); // Will remove State #2
std::cout << "Current State: " << originator.getState() << ", Version: " << originator.getVersion() << std::endl;
std::cout << "Undoing to State #3..." << std::endl;
originator.restoreStateFromMemento(caretaker.getMemento(0));
std::cout << "Current State: " << originator.getState() << ", Version: " << originator.getVersion() << std::endl;
return 0;
}Common Use Cases
- Undo/Redo Functionality: As previously shown, this is the most common use case.
- Transactions: In database or system transactions, the Memento can store the state before a transaction, allowing rollback in case of failure.
- Wizards: In wizard-style interfaces, the Memento can store the state of each step, allowing users to go back to previous steps.
Best Practices
- Immutability: The Memento should be immutable after creation to prevent external modification of the saved state.
- Narrow Interface: The Memento’s interface should be narrow, exposing only the necessary information to the Originator.
- Consider Delta Storage: If the Originator’s state is large, consider storing only the changes (delta) between states to save memory.
- Memento Ownership: Clearly define who owns the Memento objects to avoid memory leaks. Typically, the Caretaker owns the Mementos.
Common Pitfalls
- Violating Encapsulation: Avoid exposing too much internal state of the Originator in the Memento.
- Memory Leaks: Ensure that Mementos are properly deleted when they are no longer needed.
- Performance Issues: Creating and storing Mementos can be expensive. Optimize the process by using techniques like copy-on-write or delta storage.
- Thread Safety: If multiple threads access the Originator and Caretaker, ensure thread safety to prevent data corruption.
Key Takeaways
- The Memento pattern allows saving and restoring an object’s state without violating encapsulation.
- It’s commonly used for implementing undo/redo functionality.
- Careful consideration should be given to memory usage, performance, and security when using the Memento pattern.