Memento
is one of the less popular GoF patterns, that is solving a niche but important problem - how to reset the state of an object to a specified historical state. Examples include: hitting Cancel
in a dialog, transaction rollback, etc.
The below code shows how to implement mementos in idiomatic Java, with minimum boiler plate and taking advantage of the features of a modern IDE (I've used IntelliJ IDEA, if anybody creates an Eclipse version let me know - I will link to it).
- Start with a data class like this one:
class DialogModel {
int foo;
String bar;
StatefulObject stateful;
}
- Then create an inner class and copy paste the state fields:
class DialogModel {
int foo;
String bar;
StatefulObject stateful;
class Memento { // <=== this is new
int foo;
String bar;
StatefulObject stateful;
}
}
- Use multiple cursors
(
Ctrl, Ctrl, Up/Down
) to make the memento fieldsfinal
- depending on your taste you may throw in a
private
or remove existing modifiers forpackage-private
access
- depending on your taste you may throw in a
- IDEA will highlight the uninitialized finals as errors
- Go to the first error (
F2
) - Show the quick-fixes (
Alt+Enter
) and chooseAdd constructor parameters
- In the dialog that pops up select all fields (
Ctrl+A
) and accept (Enter
).
By this time the code should look like this:
class DialogModel {
int foo;
String bar;
StatefulObject stateful;
class Memento {
final int foo; // <=== these are now final
final String bar;
final StatefulObject stateful;
Memento(int foo, String bar, StatefulObject sob) { // <=== this is new
this.foo = foo;
this.bar = bar;
this.stateful = stateful;
}
}
}
- Delete the arguments of the
Memento
constructor (put the cursor between the parens, pressCtrl+W
a few times, thenDel
)- if you overshoot with the
Ctrl+W
, you may shrink the selection withCtrl+Shift+W
- if you overshoot with the
- Go to the first error (
F2
) and select the=
part of the expression - Create multiple cursors by selecting all erroneous assignments (
Ctrl+J
a few times;Ctrl+Shift+J
if you overshoot) - Get rid of the selection (
Arrow-Right
) and qualify the references with your outer-classthis
class DialogModel {
int foo;
String bar;
StatefulObject stateful;
class Memento {
final int foo;
final String bar;
final StatefulObject stateful;
Memento() { // <=== got rid of the params
this.foo = DialogModel.this.foo; // <=== added DialogModel.this
this.bar = DialogModel.this.bar;
this.stateful = DialogModel.this.stateful;
}
}
}
At this point we have captured the state of the data object. Now it is time to implement the restore method:
- Select the whole constructor (click inside and
Ctrl+W
a few times until selected) - Duplicate it (
Ctrl+D
) - Get rid of the selection with
Arrow-Left
and replace the constructor name with your restore method signature (i.e.void restore()
- As assigning to final fields is allowed only in the constructor, assignments are red again - go to the first error (
F2
) - Select
=
and do theCtrl+J
thing again to select all assignments - Dismiss the selection with
Arrow-Right
and select the right-hand-side of the assignment (Ctrl+Shift+Right
5 times) - Cut the selection and delete the
=
(Ctrl+X
, followed byBackspace
3 times) - Paste the buffers at the beginning of the line (
Home
,Ctrl+V
) and type back the '=
' - Manually take care of any stateful objects. In this case we are cloning
stateful
, but you may need to extract the state in different way (e.g. implement nested memento)
Done:
class DialogModel {
int foo;
String bar;
StatefulObject stateful;
class Memento {
final int foo;
final String bar;
final StatefulObject stateful;
Memento() {
this.foo = DialogModel.this.foo;
this.bar = DialogModel.this.bar;
this.stateful = DialogModel.this.stateful.clone(); // <=== special care of stateful data
}
void restore() { // <=== this is new
DialogModel.this.foo = this.foo;
DialogModel.this.bar = this.bar;
DialogModel.this.stateful = this.stateful;
}
}
}
The typical usage of this class would be:
assert dialog.getModel() instanceof DialogModel : "assuming we've got a dialog with model";
DialogModel.Memento dialogOriginalState = dialog.getModel().new Memento(); // store state
switch (dialog.popUpModal()) {
case OK: doSomething(dialog.getModel()); break; // accept the state change and ignore stored state
case CANCEL: dialogOriginalState.restore(); // reject state change and restore the state
}
Another case where mementos are useful is transaction processing, such as:
UnitOfWork.Memento savepoint = uow.new Memento();
try {
uow.work();
} catch(BusinessException ex) {
savepoint.restore();
throw ex;
}
Or where clients interact with a framework through a shared operation context:
Context.Memento defaultCtx = ctx.new Memento();
try {
client.describeHowToProcessData(ctx, data);
frameworkProcessData(ctx, data);
} finally {
defaultCtx.restore();
}
This last case allows for a nice declarative and expressive API, with clean separation of client and framework code.
It would look a bit more concise if the Memento
class implements AutoCloseable
, calling restore()
on close:
try (AutoCloseable ignored = ctx.new Memento()) { // <=== renamed to `ignored` so IDEA static analysis will not complain
client.describeHowToProcessData(ctx, data);
frameworkProcessData(ctx, data);
}
If we implement AutoCloseable
, we may as well want to hide the actual memento class by making it private
and adding an extra factory method to the data class:
// signatures
interface Savepointable {
AutoCloseable savepoint();
}
class FrameworkContext implements Savepointable {
...
AutoCloseable savepoint() { return new Memento(); }
class Memento implements AutoCloseable { ... }
}
// usage
try (AutoCloseable ignored = ctx.savepoint()) {
client.describeHowToProcessData(ctx, data);
frameworkProcessData(ctx, data);
}
Hopefully by now you understand what the Memento
pattern is and what is it good for - it is a specialized tool, yet occasionally it comes handy. On the other hand, the demonstrated IntelliJ IDEA shortcuts are helpful everyday, so it is worth going through the steps as an exercise.
You may notice that our implementation of the pettern differs from the Wikipedia example - by removing most of the methods and classes, we have shrunk the API surface, while preserving the use cases. This is a good thing - more code needs more documentation, carries higer chance of misuse, and statistically contains more bugs.
The pattern as-is cannot be put in a library, but it may be a fun thing to write a codegen implementing the Savepointable
interace (similar to https://github.com/google/auto). The compromise is that annotation processors require setup and often interact with tooling in weird ways, so for now I would rather suffer the occasional inconvenience of coding a pattern directly.