Introduction
Memento design pattern states that object state can be externalized and can be restored as needed. This pattern mentions three components which are object of interest i.e. Originator, Memento which wraps the originator and client that takes care of the memento object called as Caretaker.
Why do you need this pattern?
The pattern can be used in the usecase where the object state needs to be cached and restored later when needed. One example makes this more concrete is the undo/redo feature in text editors.
Let’s see the details of this pattern.
Internals of memento design pattern
There are three main components to this pattern as listed below
- The Originator is the object whose state needed to be saved.
- The Memento is the object which keeps the Originator state.
- The CareTaker(client) which takes care of the Memento objects.
Why Memento? Why cannot the Caretaker directly keep the Originator?
- This adds an extra layer of security where clients cannot access the state of the originator at all except any meta data that the Memento object might want to share.
Undo feature of text editors
Functional requirements of undo feature
- Our bare minimum text editor supports Current cursor location (X ,Y), character size, screensize, current font, contents on screen.
- Maximum character size ranges from 1 to 30.
- We only support Font type Arieal, Times new roman.
- This undo feature to restore the previous state when the user hits special key work stocks like ctrl + u.
Undo feature implementation using Java
Main.java
import working.example.tech.DesignPattern.Behavioural.MementoPattern.*;
public class Main {
public static void main(String[] args) {
UndoMiniEditorState historyFeature = new UndoMiniEditorState();
historyFeature.RunTest();
}
}
MiniEditor.java
package working.example.tech.DesignPattern.Behavioural.MementoPattern;
public class MiniEditor {
static MiniEditor inst = null;
private EditorState state;
class EditorState {
CursorPosition location;
int CurrentCharSize;
final ScreenResolution resolution = new ScreenResolution(100, 90);
Font charFont;
String word;
EditorState() {
location = new CursorPosition(0,0);
CurrentCharSize = 10;
word = "";
}
EditorState(EditorState clone) {
location = new CursorPosition(clone.location);
CurrentCharSize = clone.CurrentCharSize;
charFont = clone.charFont;
word = clone.word;
}
}
enum Font {
ARIEAL,
TIMES_NEW_ROMAN
}
MiniEditor() {
state = new EditorState();
}
class ScreenResolution {
@Override
public String toString() {
return "ScreenResolution [width=" + width + ", height=" + height + "]";
}
int width;
int height;
ScreenResolution(int width, int height) {
this.width= width;
this.height = height;
}
}
class CursorPosition {
@Override
public String toString() {
return "CursorPosition [x=" + x + ", y=" + y + "]";
}
int x;
int y;
private CursorPosition(int x, int y) {
this.x = x;
this.y = y;
}
private CursorPosition(CursorPosition clone) {
this.x = clone.x;
this.y = clone.y;
}
public void updatePosition(int xOffset, int yOffset) {
this.x += xOffset;
this.y += yOffset;
}
}
private void restoreParameters(MiniEditor.EditorState old) {
this.state.location = old.location;
this.state.CurrentCharSize = old.CurrentCharSize;
this.state.charFont = old.charFont;
this.state.word = old.word;
}
public void setFont(Font in) {
this.state.charFont = in;
}
//This can be improved as per need
public void writeWord(String word) {
this.state.word += word;
int len = word.length();
if ((len + (this.state.location.x)) >= this.state.resolution.width) {
this.state.location.updatePosition((len - this.state.location.x), 1);
} else {
this.state.location.updatePosition(len, this.state.location.y);
}
}
public Snapshot save() {
return new Snapshot(new EditorState(this.state));
}
public void restore(Snapshot snap) {
this.restoreParameters(snap.getState());
}
public static MiniEditor getInstance() {
if (inst == null) {
inst = new MiniEditor();
}
return inst;
}
@Override
public String toString() {
return "MiniEditor [location=" + this.state.location + ", CurrentCharSize=" + state.CurrentCharSize + ", resolution="
+ state.resolution + ", charFont=" + state.charFont + ", ContentsInEditor=" + state.word + "]";
}
}
Snapshot.java
package working.example.tech.DesignPattern.Behavioural.MementoPattern;
public class Snapshot {
private MiniEditor.EditorState state;
public MiniEditor.EditorState getState() {
return state;
}
public Snapshot(MiniEditor.EditorState state) {
this.state = state;
}
}
UndoMiniEditorState.java
package working.example.tech.DesignPattern.Behavioural.MementoPattern;
import java.util.Stack;
public class UndoMiniEditorState implements working.example.tech.interfaces.TestCaseRunner {
private Stack<Snapshot> history;
private MiniEditor editor;
public UndoMiniEditorState() {
history = new Stack<>();
editor = MiniEditor.getInstance();
}
@Override
public void RunTest() {
editor.setFont(MiniEditor.Font.ARIEAL);
editor.writeWord("memento");
System.out.println("[1] Current state of Editor: " + editor.toString());
history.push(editor.save());
editor.writeWord(" design");
System.out.println("[2] Current state of Editor: " + editor.toString());
history.push(editor.save());
editor.writeWord(" pattern");
System.out.println("[3] Current state of Editor: " + editor.toString());
editor.restore(history.pop());
System.out.println("[2] Restoring Previous state"+ editor.toString());
editor.restore(history.pop());
System.out.println("[1] Restoring Previous state"+ editor.toString());
}
@Override
public void showOut() {
// TODO Auto-generated method stub
}
}
Output
[1] Current state of Editor: MiniEditor [location=CursorPosition [x=7, y=0], CurrentCharSize=10, resolution=ScreenResolution [width=100, height=90], charFont=ARIEAL, ContentsInEditor=memento]
[2] Current state of Editor: MiniEditor [location=CursorPosition [x=14, y=0], CurrentCharSize=10, resolution=ScreenResolution [width=100, height=90], charFont=ARIEAL, ContentsInEditor=memento design]
[3] Current state of Editor: MiniEditor [location=CursorPosition [x=22, y=0], CurrentCharSize=10, resolution=ScreenResolution [width=100, height=90], charFont=ARIEAL, ContentsInEditor=memento design pattern]
[2] Restoring Previous state: MiniEditor [location=CursorPosition [x=14, y=0], CurrentCharSize=10, resolution=ScreenResolution [width=100, height=90], charFont=ARIEAL, ContentsInEditor=memento design]
[1] Restoring Previous state: MiniEditor [location=CursorPosition [x=7, y=0], CurrentCharSize=10, resolution=ScreenResolution [width=100, height=90], charFont=ARIEAL, ContentsInEditor=memento]
Conclusion
In this blog we discussed memento design pattern in detail with an example of undo feature. Hope this helps and you will be able to apply this in any of your upcoming features. We have a lot more design patterns to cover so, stay tuned as more blogs coming up!