[design mode] memo pattern

Posted by crazy/man on Tue, 21 Dec 2021 20:32:47 +0100

 

🔥 core

Memo mode captures the internal state of an object and saves the state outside the object,

This allows you to restore the object to its original saved state later.

 

🙁 Problem scenario

You are a poster designer twenty years ago. Recently, a powerful graphics editing software (PS) was born, and you became the first batch of PS testers.

You are amazed by this new powerful tool. However, you have found a fatal problem - when you mistakenly perform an operation, you can only perform one or more operations again to offset its effect.

If only I could undo this operation, or go back to the previous historical version. You said to yourself.

Whether it is undo operation or rollback version, its implementation principle is the same: the program will record the current state of the object and store it in the history record; If you want to use the history version, you can directly modify the status of the current object according to the history. The saved status information is called "snapshot".

How to generate a snapshot?

Idea 1) traverse all member variables of the object and copy and save their values. Unfortunately, most objects set the member variable private and do not provide methods to expose it.

Train of thought 2) suppose that the objects are like hippies and like open social relations, so they will disclose all their member variables. However, this seems to bring more serious security problems.

We seem to be walking into a dead end: we will either restrict access to its state and fail to generate snapshots, or expose all the internal details of the class and make it too fragile

 

🙂 Solution

We made a fatal mistake - always trying to break the package. Why not try to think from the "inside"?

Memo mode places the snapshot creation inside the original object. An object certainly has full access to itself, even private.

The "snapshot" we only talk about is actually the state information of the object (the collection of values of member variables). Now we encapsulate it into a special object called memo; For each memo, we put it into a special object called memo history. These two special objects have a common feature: there is no way to expose internal information.

That is, we capture the internal state of the object and save the state outside the object; Saving in the external state does not expose its own information. In the fallback state, it is still implemented by external calls to the internal.

The above sentence is hard to understand. Here is an image example: the original object will give his state as a secret to the memo object. The memo object will strictly keep this secret and will not be known by anyone; Because the memo object has a reference to the original object, you can return the secret directly to the original object.

Congratulations, you have discovered the secret of Ctrl + z!

 

🌈 Interesting examples

Sometimes there are disgusting bugs in the game, such as missing map plates, abnormal equipment parameters, and items that cannot be dropped. At this point, you need to immediately roll back the game version.

The game itself corresponds to the original object, the game version corresponds to the memo, and the game version history corresponds to the memo history. After clarifying this structure, the game version fallback function is not difficult to implement.

 game
class Game {
    // Version information
    private int versionId;
    private String versionName;

    public void setVersionId(int versionId) { this.versionId = versionId; }
    public void setVersionName(String versionName) { this.versionName = versionName; }

    // Generate version snapshot (critical)
    public Version createVersion() {
        return new Version(this, versionId, versionName);
    }

    public void showVersion() {
        System.out.println("[" + versionId + "]" + versionName);
    }
}


 Game version
class Version {
    // Reference to the original object
    Game game;
    // Information used to roll back the original object version
    private int versionId;
    private String versionName;

    public Version(Game game, int versionId, String versionName) {
        this.game = game;
        this.versionId = versionId;
        this.versionName = versionName;
    }

    // Fallback original object version (key)
    public void rollbackVersion() {
        game.setVersionId(versionId);
        game.setVersionName(versionName);
    }
}


 Game version history
class VersionHistory {
    // This is a stack data structure
    private Deque<Version> versionStack = new ArrayDeque<>();

    // Stack a version
    public void push(Version version) {
        versionStack.push(version);
    }

    // Stack a version
    public Version pop() {
        return versionStack.pop();
    }
}
public class MementoPatternDemo {
    public static void main(String[] args) {

        // Create a GameObject (original object)
        Game game = new Game();
        // Create a game version history object (Memo history object)
        VersionHistory versionHistory = new VersionHistory();

        // Set the current game version and put the version snapshot (memo) on the stack
        game.setVersionId(100);
        game.setVersionName("version-100");
        game.showVersion();
        versionHistory.push(game.createVersion());

        // Set the current game version and put the version snapshot (memo) on the stack
        game.setVersionId(200);
        game.setVersionName("version-200");
        game.showVersion();
        versionHistory.push(game.createVersion());

        // Set the current game version and put the version snapshot (memo) on the stack
        game.setVersionId(300);
        game.setVersionName("version-300");
        game.showVersion();
        versionHistory.push(game.createVersion());

        // Version snapshot (memo) out of the stack, used to roll back the game version
        versionHistory.pop().rollbackVersion();
        game.showVersion();

        // Version snapshot (memo) out of the stack, used to roll back the game version
        versionHistory.pop().rollbackVersion();
        game.showVersion();

        // Version snapshot (memo) out of the stack, used to roll back the game version
        versionHistory.pop().rollbackVersion();
        game.showVersion();
    }
}
[100]version-100
[200]version-200
[300]version-300
[300]version-300
[200]version-200
[100]version-100

 

☘️ Usage scenario

◾ When you need to create an object state snapshot to restore its previous state, you can use memo mode.

Memo mode allows you to copy all States in an object (including private member variables) and save them independently of the object. Although most people remember this mode because of the use case of "undo", it is also essential in the process of dealing with transactions (such as rolling back an operation when an error occurs).

◾ This mode can be used when directly accessing the member variable, getter or setter of the object will cause the encapsulation to break through.

Memos let objects take responsibility for creating snapshots of their state. No other object can read the snapshot, which effectively ensures the security of the data.

 

🧊 Implementation mode

(1) Determine the class that plays the role of initiator. It is important to specify whether the program uses one initiator central object or multiple smaller objects.

(2) Create memo class. Declare memo member variables corresponding to each initiator member variable one by one.

(3) Make the memo class immutable. Memos can only receive data once through the constructor. This class cannot contain setters.

(4) If the programming language you use supports nested classes, you can nest memos in the originator; if not, you can extract an empty interface from the memo class and let all other objects reference memos through the interface. You can add some metadata operations to the interface, but you can't expose the state of the originator.

(5) Add a method to the originator to create a memo. The originator must pass its state to the memo through one or more actual parameters of the memo constructor.

(6) The type of the result returned by this method must be the interface you extracted in the previous step (if you have already extracted). In fact, the method that creates the memo must interact directly with the memo class.

(7) Add a method to the originator class to restore its own state. This method accepts the memo object as a parameter. If you extracted the interface in the previous step, you can use the interface as the type of the parameter. In this case, you need to force the input object to memo, because the originator needs to have full access to the object.

(8) Whether the owner is a command object, history, or something completely different, it must know when to request a new memo from the initiator, how to store the memo, and when to use a specific memo to recover the initiator.

(9) The connection between the owner and the originator can be moved to the memo class. In this example, each memo must be connected to the originator that created it. The recovery method can also be moved to the memo class, but this method can only be used when the memo class is nested in the originator, or the originator class provides enough setters and can override its state Can be achieved.

 

🎲 Advantages and disadvantages

  ➕ You can create object state snapshots without destroying object encapsulation.

  ➕ You can simplify the originator code by having the owner maintain the originator status history.

  ➖ If the client creates memos too often, the program will consume a lot of memory.

  ➖ The owner must fully track the life cycle of the initiator in order to destroy the discarded memo.

  ➖ Most dynamic programming languages (such as PHP, Python, and JavaScript) cannot ensure that the state in the memo is not modified.

 

🌸 supplement

  a memo can be an internal class of the initiator, so that the logic will be clearer.

  memo history is a memo management class, which is optional. In addition, it is more in line with Demeter's law.

 

🔗 Reference website

Topics: Java Design Pattern UML