When we want to store an object's state without exposing internal details about the state then we can use memento design pattern.
The main intent behind saving state is often because we want to restore the object to a saved state
Using memento we can ask an object to give its state as a single, sealed object and store it for later use. This object should not expose the state for modification
This pattern is often combined with Command design pattern to provide undo functionality in an application.
2. Description
2.1 Implementation
We start by finding originator state which is to be stored in memento
We then implement the memento with requirement that it can't be changed and read outside the originator
Originator provides a method to get its current snapshot out, which will return an instance of memento
Another method in originator takes a memento object as argument and the originator object resets itself to match with the state in memento
2.2 Consideration
2.2.1 Implementation
It is important to keep an eye on the size of state stored in memento. A solution for discarding order state may be needed to handle large memory consumption scenarios
Memento often ends up being an inner class due to the requirement that it must encapsulate all details of what is stored in its instance
Resetting to the previous state should consider effects on states of other objects/services
2.2.2 Design
If there is a definite, fixed way in which mementos are created then we can only store incremental state in mementos. This is especially true if we are using command design pattern where every command stores a memento before execution
Mementos can be stored internally by the originator as well but this complicates the originator. An external caretaker with fully encapsulated Memento provides you with more flexibility in implementation.
2.3 Pitfalls
In practice, creating a snapshot of state may not be easy if other objects are part of originator's state
Resetting a state may not be as simple as copying references. If state change of originator is tied with other parts of an application then those parts may become out of sync/invalid due to resetting state.
3. Usage
The undo support provided by the javax.swing.text.JTextComponent and its child classes like JTextField, JTextArea, and etc.
The javax.swing.undo.UndoManager acts as the caretaker and implementations of javax.swing.undo.UndoableEdit interface work as mementos. The javax.swing.text.Document implementation which is model for text components in swing is the originator.
4. Comparison with Command
Memento
Command
State of memento is sealed for everyone except originator
Although commands are typically immutable their state is often readable.
A memento needs to be stored for it to be of any use
Commands can be stored as well but not storing them after execution is optional
5. Example
class Workflow {
private LinkedList<String> steps;
private String name;
public Workflow(String name) {
this.name = name;
this.steps = new LinkedList<>();
}
public Workflow(String name, String... steps) {
this.name = name;
this.steps = new LinkedList<>();
if(steps != null && steps.length > 0) {
Arrays.stream(steps).forEach(s->this.steps.add(s));
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("Workflow [name=");
builder.append(name).append("]\nBEGIN -> ");
for(String step : steps) {
builder.append(step).append(" -> ");
}
builder.append("END");
return builder.toString();
}
public void addStep(String step) {
steps.addLast(step);
}
public boolean removeStep(String step) {
return steps.remove(step);
}
public String[] getSteps() {
return steps.toArray(new String[steps.size()]);
}
public String getName() {
return name;
}
}
class WorkflowDesigner {
private Workflow workflow;
public void createWorkflow(String name) {
workflow = new Workflow(name);
}
public Workflow getWorkflow() {
return this.workflow;
}
public Memento getMemento() {
if(workflow == null) {
return new Memento();
}
return new Memento(workflow.getSteps(), workflow.getName());
}
// store states
public void setMemento(Memento memento) {
if(memento.isEmpty()) {
this.workflow = null;
} else {
// reset exact equal to memento
this.workflow = new Workflow(memento.getName(), memento.getSteps());
}
}
public void addStep(String step) {
workflow.addStep(step);
}
public void removeStep(String step) {
workflow.removeStep(step);
}
public void print() {
System.out.println(workflow);
}
// memento
// snapshot of workflow designer
public class Memento {
/* we have to copy states deeply*/
private String[] steps;
private String name;
private Memento() {}
// Only designer create memento
private Memento(String[] steps, String name) {
this.steps = steps;
this.name = name;
}
// prevent changing and seeing states
private String[] getSteps() {
return steps;
}
private String getName() {
return name;
}
private boolean isEmpty() {
return this.getSteps() == null && this.getName() == null;
}
}
}
interface WorkflowCommand {
void execute();
void undo();
}
abstract class AbstractWorkflowCommand implements WorkflowCommand {
protected WorkflowDesigner.Memento memento;
protected WorkflowDesigner receiver;
public AbstractWorkflowCommand(WorkflowDesigner designer) {
this.receiver = designer;
}
@Override
public void undo() {
receiver.setMemento(memento);
}
}
class RemoveStepCommand extends AbstractWorkflowCommand {
private String step;
public RemoveStepCommand(WorkflowDesigner designer, String step) {
super(designer);
this.step = step;
}
@Override
public void execute() {
memento = receiver.getMemento();
receiver.removeStep(step);
}
}
class CreateCommand extends AbstractWorkflowCommand {
private String name;
public CreateCommand(WorkflowDesigner designer, String name) {
super(designer);
this.name = name;
}
@Override
public void execute() {
this.memento = receiver.getMemento();
receiver.createWorkflow(name);
}
}
class AddStepCommand extends AbstractWorkflowCommand {
private String step;
public AddStepCommand(WorkflowDesigner designer, String step) {
super(designer);
this.step = step;
}
@Override
public void execute() {
this.memento = receiver.getMemento();
receiver.addStep(step);
}
}
public class Client {
public static void main(String[] args) {
WorkflowDesigner designer = new WorkflowDesigner();
LinkedList<WorkflowCommand> commands = runCommands(designer);
designer.print();
commands.removeLast().undo();
designer.print();
commands.removeLast().undo();
designer.print();
}
private static void undoLastCommand(LinkedList<WorkflowCommand> commands) {
if(!commands.isEmpty())
commands.removeLast().undo();
}
private static LinkedList<WorkflowCommand> runCommands(WorkflowDesigner designer) {
LinkedList<WorkflowCommand> commands = new LinkedList<>();
WorkflowCommand cmd = new CreateCommand(designer,"Leave Workflow");
commands.addLast(cmd);
cmd.execute();
cmd = new AddStepCommand(designer,"Create Leave Application");
commands.addLast(cmd);
cmd.execute();
cmd = new AddStepCommand(designer,"Submit Application");
commands.addLast(cmd);
cmd.execute();
cmd = new AddStepCommand(designer,"Application Approved");
commands.addLast(cmd);
cmd.execute();
return commands;
}
}