The document discusses implementing the Memento pattern in the Exparency BPM system to provide undo/redo functionality. It describes using two queues - a command queue and an undo queue - to store commands. Command objects save the old and new state and are serialized/deserialized. A CommandManager interacts with these queues and localStorage to persist commands. This allows recorded macros to reproduce issues for debugging.
1. Document prepared by Steve Widom of Exparency, LLC.
Implementing the Memento
Pattern in the Exparency BPM
System
Steve Widom, June, 2016
Introduction
The front end to the Exparency BPM (Business Process Management) system is a drawing tool in the
spirit of 2D CAD (schematic entry systems in the electrical world, ERD diagrams in the data modeling
world, etc.) The graphical primitives are shapes such as rectangles, circles, lines and text. What
differentiates the graphics from a Visio diagram, for example, is the fact that these geometries have
semantic interpretation in the sense that the shapes are connected to each other and these shapes and
connections have meaning.
Although drawing tools, or any other PC or Mac application for that matter, have CTRL-Z and CTRL-Y
functionality out of the box, it is rare to find that kind of functionality on a SaaS application. It is a
requirement of the Exparency BPM system to provide that familiar functionality and the purpose of this
document is to explain the architecture behind how this is achieved.
We will conclude the document by discussing a fortunate consequence of implementing the Memento
pattern – namely, adding Macro functionality that can be stored and replayed, while also being used as
a debugging tool for customer cases.
The Memento Pattern
CTRL-Z (oftentimes known as edit undo) and CTRL-Y (oftentimes known as edit redo) are the tenets of
the Memento Pattern. This pattern is well documented in the Gang of Four famous treatise that
exploded the whole Design Pattern industry in computer science and is diagramed right from that
source below:
2. Document prepared by Steve Widom of Exparency, LLC.
In a nutshell the entity undergoing change (the Originator) carries its state while the Memento object
saves the state at various points in time. It is the responsibility of the Caretaker to define those points
in time. For example, we are typically familiar with editing a text field. The Caretaker does not save
the state of that text field upon every character entered. It will, however, save that state when the
user hits enter or when the field loses focus.
While this is all well and good, the author tends to look at the edit undo/redo functionality slightly
differently as shown below.
Very simply, two queues can be easily implemented to store commands and their undoing. As the user
enters new commands they are placed on top of the Command queue as a LIFO (Last in/First out).
When the user issues an edit undo (CTRL-Z) the last entered command is popped off of the Command
queue and then it is placed on the Undo queue which is also configured as a LIFO. Similarly when the
user issues a redo (CTRL-Y) the last entered command is popped off of the Undo queue and then it is
placed back onto the Command queue.
The elegance and simplicity of this diagram hides fundamental features that are going on behind the
scenes:
1. When a command is added to the Command queue, the old state and the new state need to be
placed into that command object.
2. When an undo request is made the application must recover the old state stored in the
command object that was popped off of the Command queue and placed onto the Undo queue
– thus achieving the desired inverse effect (i.e. undo.)
The following sequence diagram represents this course of events:
3. Document prepared by Steve Widom of Exparency, LLC.
Notice that we have introduced a third actor into the mix – the Command Manager which encapsulates
the management of the two queues.
Implementation
The Command Manager is a modified version of Javascript code, originally written by Alexander Brevig
and available on GitHub. This modification is simply to use the HTML5 localStorage to store all
commands, in addition to being added to the managed command queue. There are several advantages
to this, the main one being persistence of a session of activity on the local client that can be restored
at any time without the penalty of a network hop and a database lookup. We will talk more about this
functionality towards the end of this document.
The Javascript code for this module is shown below:
var CommandManager = (function () {
function CommandManager() {}
CommandManager.executed = [];
CommandManager.unexecuted = [];
CommandManager.execute = function execute(globalService, cmd) {
cmd.execute(globalService);
CommandManager.executed.push(cmd);
};
CommandManager.undo = function undo(globalService) {
var cmd1 = CommandManager.executed.pop();
if (cmd1 !== undefined){
if (cmd1.unexecute !== undefined){
cmd1.unexecute(globalService);
}
CommandManager.unexecuted.push(cmd1);
4. Document prepared by Steve Widom of Exparency, LLC.
}
};
CommandManager.redo = function redo(globalService) {
var cmd2 = CommandManager.unexecuted.pop();
if (cmd2 === undefined){
cmd2 = CommandManager.executed.pop();
// This is not a code error. We are executing the last command twice because there is
// no command found on the CTRL-Y (undo) stack. Apparently this is conventional, and
// we are going to execute the last command one more time, which means we need to push
// it to the command stack twice since that is what we are doing.
CommandManager.executed.push(cmd2);
CommandManager.executed.push(cmd2);
}
if (cmd2 !== undefined){
cmd2.execute(globalService);
CommandManager.executed.push(cmd2);
}
};
CommandManager.saveCommandToLocalStorageCommandQueue = function(cmd) {
var currentCommandQueue = localStorage.getItem('CommandQueue');
if (currentCommandQueue == null) {
currentCommandQueue = [];
}
else {
currentCommandQueue = JSON.parse(currentCommandQueue);
}
var serialized = cmd.serialize();
currentCommandQueue.push(serialized);
var json = JSON.stringify(currentCommandQueue);
localStorage.setItem('CommandQueue', json);
};
CommandManager.getCommandsFromLocalStorageCommandQueue = function() {
var currentCommandQueue = localStorage.getItem('CommandQueue');
if (currentCommandQueue == null) {
currentCommandQueue = [];
}
else {
currentCommandQueue = JSON.parse(currentCommandQueue);
}
return currentCommandQueue;
};
return CommandManager;
})();
Notice that the Command Manager is implemented as a Singleton (yet another design pattern.) There
are a couple of points to be made about this code snippet. The first is that localStorage can only hold
string name/value pairs. Therefore when we add new commands to localStorage for future use, we
5. Document prepared by Steve Widom of Exparency, LLC.
deserialize the entire command array string representation back to a Javascript array, add the new
command to this array, and then re-serialize that object back to a string and store that string in
localStorage. Admittedly this can get expensive as the volume of commands entered gets large and in
a future version a different methodology will probably be employed. Given that the intent is to store
these commands in a backend database (perhaps a MongoDB document) as a Macro definition, it may
end up being a moot point anyway.
At the bottom of the class hierarchy is the command object which is a Javascript function that carries
all of the data that is needed to execute a command as well as undo the effects of a command. We
will use a swimlane as an example (in workflow systems it is common to group tasks assigned to a role
by physically placing those tasks in what is called a swimlane):
function EditSwimlaneCommand (data) {
this.oldSwimlaneName = data.oldSwimlaneName;
this.newSwimlaneName = data.newSwimlaneName;
this.oldFillColor = data.oldFillColor;
this.newFillColor = data.newFillColor;
this.oldOpacity = data.oldOpacity;
this.newOpacity = data.newOpacity;
};
EditSwimlaneCommand.prototype = {
constructor: EditSwimlaneCommand,
init: function() {
},
execute: function(globalService) {
globalService.executeEditSwimlaneCommand(this);
},
unexecute: function(globalService) {
globalService.unexecuteEditSwimlaneCommand(this);
},
serialize: function() {
return JSON.stringify({
objectType: "EditSwimlaneCommand",
data:{
oldSwimlaneName: this.oldSwimlaneName,
newSwimlaneName: this.newSwimlaneName,
oldFillColor: this.oldFillColor.toCSS(true),
newFillColor: this.newFillColor.toCSS(true),
oldOpacity: this.oldOpacity,
newOpacity: this.newOpacity
}
});
},
deserialize: function(jsonData) {
var data = JSON.parse(jsonData);
var cmd = new EditSwimlaneCommand(data.data);
return cmd;
}
6. Document prepared by Steve Widom of Exparency, LLC.
};
Notice that this Javascript object carries two sets of data – the new state as well as the old state. It
would not be possible to undo this command without knowledge of the old state. Also notice that this
object has the responsibility for serializing itself so that it can be placed on the respective queues as
well as localStorage as a string, as well as restoring itself from such a string representation.
Finally, notice that the an AngularJS service is engaged to perform the actual work on behalf of the
application to execute the command and to undo the command (via a call to the object’s unexecute
method.) This service is generally global to the entire application and thus is named as such
(globalService) and it has various methods for interacting with the various application controllers as
well as executing the REST API services. Being over 600 lines of Javascript AngularJS code, it is beyond
the scope of this document to show all that detail. Suffice it to say that methods related to pulling
drawing data from the backend MySQL database (fill colors, border widths and colors, coordinate data,
widths, heights, etc.) along with the other expected CRUD operations are the responsibility of this
service. Since this document is focused on the implementation of the Memento Pattern it is not
necessary to show all that detail.
We can now summarize our implementation pattern for achieving our goal of supporting CTRL-Z and
CTRL-Y:
1. For each command in the application create a command object such as the one shown above.
2. In the application controller add code that populates this command object with the new state
as well as the original state. The following code snippet is an example that relates to the
swimlane example just mentioned. Note that it uses Google’s Material UI Dialog to capture the
new swimlane data (name, fill color and opacity).
function EditSwimlaneDialogController($scope, $mdDialog, data) {
var self = this;
self.fillColor = data.swimlane.children[0].fillColor.toCSS(true);
self.opacity = data.swimlane.children[0].opacity;
self.name = data.swimlane.swimlaneName;
$scope.hide = function() {
$mdDialog.hide();
};
$scope.cancel = function() {
$mdDialog.cancel();
};
$scope.submitEditSwimlaneForm = function() {
var json = {
oldSwimlaneName: data.swimlane.swimlaneName,
newSwimlaneName: self.name,
oldFillColor:
data.swimlane.children[0].fillColor.toCSS(true),
newFillColor: self.fillColor,
oldOpacity: data.swimlane.children[0].opacity,
newOpacity: self.opacity
};
data.globalService.executeEditSwimlaneCommand(new
EditSwimlaneCommand(json));
$mdDialog.hide();
};
}
7. Document prepared by Steve Widom of Exparency, LLC.
3. Create a method in the globalService service that executes and unexecutes this command
object (possibly making asynchronous REST calls.) Going back to the same example of the
swimlane, here is a code snippet for the implementation of the
globalService.executeEditSwimlaneCommand() method:
executeEditSwimlaneCommand: function(cmd) {
this.pushCommand(cmd);
var swimlane = this.getDrawing().getSwimlane(this, cmd.oldSwimlaneName);
swimlane.fillColor = cmd.newFillColor;
swimlane.opacity = cmd.newOpacity;
this.getDrawing().changeSwimlaneName(this, swimlane,
cmd.newSwimlaneName);
this.getDrawing().refreshView(this);
},
Finally, the pushCommand method does the work of interacting with the Command Manager:
pushCommand: function(cmd) {
CommandManager.executed.push(cmd);
if (this.getMacroRecordState() == 'Record') {
CommandManager.saveCommandToLocalStorageCommandQueue(cmd);
}
},
Notice that this last code snippet has a logical test to determine whether the application is in Record
mode. We only want to store commands in localStorage if we are, indeed, recording these events.
A Word About Macros
In the Introduction of this document it was mentioned that a positive side effect of the implementation
of the Memento Pattern was the ability to record and playback macros that are stored in localStorage.
Long term the intent is to store these collections of commands as named macros in the back end
database. This should be a significant benefit to the user.
Additionally, however, there is a somewhat “selfish” reason for adding this functionality and that is
related to customer incidence reporting of a bug or general problem. Oftentimes a CSR or developer,
upon initiating a conversation with the customer, will naturally ask what they were doing at the time
that the bug or problem occurred. More likely than not the customer will not have those exact details
(why would they?) However, if the CSR or developer asks that the customer send them the command
queue stored in localStorage (effectively an event log with all the necessary detail) then the developer
can likely reproduce the problem in a development environment with a debugger and all. This will
hopefully turn out to be a very useful tool in diagnosing customer complaints. Only time will tell.