Skip to content

Instantly share code, notes, and snippets.

@ddimtirov
Last active October 19, 2017 13:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ddimtirov/20809a764b4b3dd2e641af0059882774 to your computer and use it in GitHub Desktop.
Save ddimtirov/20809a764b4b3dd2e641af0059882774 to your computer and use it in GitHub Desktop.
Creating mementos and demonstrating advanced code editing with with IntelliJ IDEA

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).

Instructions

  1. Start with a data class like this one:
class DialogModel {
  int foo;
  String bar;
  StatefulObject stateful;
}
  1. 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;    
  }
}
  1. Use multiple cursors (Ctrl, Ctrl, Up/Down) to make the memento fields final
    • depending on your taste you may throw in a private or remove existing modifiers for package-private access
  2. IDEA will highlight the uninitialized finals as errors
  3. Go to the first error (F2)
  4. Show the quick-fixes (Alt+Enter) and choose Add constructor parameters
  5. 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;
        }
    }
}
  1. Delete the arguments of the Memento constructor (put the cursor between the parens, press Ctrl+W a few times, then Del)
    • if you overshoot with the Ctrl+W, you may shrink the selection with Ctrl+Shift+W
  2. Go to the first error (F2) and select the = part of the expression
  3. Create multiple cursors by selecting all erroneous assignments (Ctrl+J a few times; Ctrl+Shift+J if you overshoot)
  4. Get rid of the selection (Arrow-Right) and qualify the references with your outer-class this
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:

  1. Select the whole constructor (click inside and Ctrl+W a few times until selected)
  2. Duplicate it (Ctrl+D)
  3. Get rid of the selection with Arrow-Left and replace the constructor name with your restore method signature (i.e. void restore()
  4. As assigning to final fields is allowed only in the constructor, assignments are red again - go to the first error (F2)
  5. Select = and do the Ctrl+J thing again to select all assignments
  6. Dismiss the selection with Arrow-Right and select the right-hand-side of the assignment (Ctrl+Shift+Right 5 times)
  7. Cut the selection and delete the = (Ctrl+X, followed by Backspace 3 times)
  8. Paste the buffers at the beginning of the line (Home, Ctrl+V) and type back the '='
  9. 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;
        }
    }
}

Usage

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
}

Variations

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);
}

Conclusion

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment