© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
D. NesterukDesign Patterns in .NET 6https://doi.org/10.1007/978-1-4842-8245-8_20

20. Memento

Dmitri Nesteruk1  
(1)
St. Petersburg, c.St-Petersburg, Russia
 

When we looked at the Command design pattern, we noted that recording a list of every single change theoretically allows you to roll back the system to any point in time – after all, you’ve kept a record of all the modifications.

Sometimes, though, you don’t really care about playing back through all the states of the system, but you do care about being able to roll back the system to a particular state, if need be.

This is precisely what the Memento pattern does: it typically stores the state of the system and returns it as a dedicated, read-only object with no behavior of its own. This “token,” if you will, can be used only for feeding it back into the system to restore it to the state it represents.

Let’s look at an example.

Bank Account

Let’s use an example of a bank account that we’ve made before…
public class BankAccount
{
  private int balance;
  public BankAccount(int balance)
  {
    this.balance = balance;
  }
  // todo: everything else :)
}
…but now we decide to make a bank account with a Deposit(). Instead of it being void as in previous examples, Deposit() will now be made to return a Memento
public Memento Deposit(int amount)
{
  balance += amount;
  return new Memento(balance);
}
…and the memento will then be usable for rolling back the account to the previous state:
public void Restore(Memento m)
{
  balance = m.Balance;
}
As for the memento itself, we can go for a trivial implementation:
public class Memento
{
  public int Balance { get; }
  public Memento(int balance)
  {
    Balance = balance;
  }
}

You’ll notice that the Memento class is immutable. Imagine if you could, in fact, change the balance: you could roll back the account to a state it was never in!

And here is how one would go about using such a setup:
var ba = new BankAccount(100);
var m1 = ba.Deposit(50);
var m2 = ba.Deposit(25);
WriteLine(ba); // 175
// restore to m1
ba.Restore(m1);
WriteLine(ba); // 150
// restore back to m2
ba.Restore(m2);
WriteLine(ba); // 175

This implementation is good enough, through there are some things missing. For example, you never get a memento representing the opening balance because a constructor cannot return a value. You could add an out parameter, of course, but that’s just too ugly.

Undo and Redo

What if you were to store every memento generated by BankAccount? In this case, you’d have a situation similar to our implementation of the Command pattern, where undo and redo operations are a byproduct of this recording. Let’s see how we can get undo/redo functionality with a memento.

We’ll introduce a new BankAccount class that’s going to keep hold of every single memento it ever generates:
public class BankAccount
{
  private int balance;
  private List<Memento> changes = new();
  private int current;
  public BankAccount(int balance)
  {
    this.balance = balance;
    changes.Add(new Memento(balance));
  }
}

We have now solved the problem of returning to the initial balance: the memento for the initial change is stored as well. Of course, this memento isn’t actually returned, so in order to roll back to it, well, I suppose you could implement some Reset() function or something – totally up to you.

The BankAccount class has a current member that stores the index of the latest memento. Hold on. Why do we need this? Isn’t it the case that current will always be one less than the list of changes? Only if you want to support undo/rollback operations; if you want redo operations, too, you need this!

Now, here’s the implementation of the Deposit() method:
public Memento Deposit(int amount)
{
  balance += amount;
  var m = new Memento(balance);
  changes.Add(m);
  ++current;
  return m;
}
There are several things that happen here:
  • The balance is increased by the amount you want to deposit.

  • A new memento is constructed with the new balance and added to the list of changes.

  • We increase the current value (you can think of it as a pointer into the list of changes).

Now here comes the fun stuff. We add a method to restore the account state based on a memento:
public void Restore(Memento m)
{
  if (m != null)
  {
    balance = m.Balance;
    changes.Add(m);
    current = changes.Count - 1;
  }
}

The restoration process is significantly different from the one we’ve looked at earlier. First, we actually check that the memento is initialized – this is relevant because we now have a way of signaling no-ops: just return a default value. Also, when we restore a memento, we actually add that memento to the list of changes so an undo operation will work correctly on it.

Now, here is the (rather tricky) implementation of Undo():
public Memento Undo()
{
  if (current > 0)
  {
    var m = changes[--current];
    balance = m.Balance;
    return m;
  }
  return null;
}

We can only Undo() if current points to a change that is greater than zero. If that’s the case, we move the pointer back, grab the change at that position, apply it, and then return that change. If we cannot roll back to a previous memento, we return null, which should explain why we check for null in Restore().

The implementation of Redo() is very similar:
public Memento Redo()
{
  if (current + 1 < changes.Count)
  {
    var m = changes[++current];
    balance = m.Balance;
    return m;
  }
  return null;
}
Again, we need to be able to redo something: if we can, we do it safely; if not, we do nothing and return null. Putting it all together, we can now start using the undo/redo functionality:
var ba = new BankAccount(100);
ba.Deposit(50);
ba.Deposit(25);
WriteLine(ba);
ba.Undo();
WriteLine($"Undo 1: {ba}"); // Undo 1: 150
ba.Undo();
WriteLine($"Undo 2: {ba}"); // Undo 2: 100
ba.Redo();
WriteLine($"Redo 2: {ba}"); // Redo 2: 150

Memento and Command

As I’m sure you’ve guessed, the creation of mementoes for every single change in the system is quite often unrealistic. But we’ve already seen the way undo/redo operations are defined in the Command design pattern. Just to recap, our approach to defining undo operations for Command basically meant that we would do the opposite of what the operation entailed. So, for a deposit operation, Undo() would take that money out of the account.

This is not always a realistic option either. Sometimes, the effects of an executed command are far-reaching and difficult to predict. Thus, it would make sense to use the Memento pattern to preserve the entire state of the system.

We can therefore put the Command and Memento approaches together as follows:
public class WithdrawCommand : ICommand
{
  public BankAccount Account;
  public decimal Amount;
  private Memento memento;
  private bool succeeded;
  public override void Call()
  {
    succeeded = Account.Withdraw(Amount);
    memento = Account.Snapshot(); // memento-creating method
  }
  public override void Undo()
  {
    if (succeeded && memento != null)
    {
      Account.RestoreTo(memento);
      memento = null; // prevent second undo
    }
  }
}

One might argue that this approach simply added an extra level of indirection: instead of saving the state of the account directly, it is saved indirectly through the use of a memento. In this example, though, instead of having individual operations return memento objects, we create a memento explicitly. This process, in turn, could also leverage the Prototype pattern in situations where the state of the object you’re trying to preserve is so complicated that writing explicit serialization code is tedious. But if that is the approach you take, you must distinguish between two types of data: the salient information about the system that must be persisted and any temporary information (e.g., private fields storing some temporary indicators) that need not be. The best advice I can give is to put all temporary information on the stack as return values – that way, you don’t hold on to it longer than necessary.

Summary

The Memento pattern is all about handing out tokens that can be used to restore the system to a prior state. Typically, the token contains all the information necessary to move the system to a particular state, and if it’s small enough, you can also use it to record all the states of the system so as to allow not just the arbitrary resetting of the system to a prior state, but controlled navigation backward (undo) and forward (redo) of all the states the system was in.

One design decision that I made in the preceding demos is to make the memento a class. This allows me to use the null value to encode the absence of a memento to operate upon. If we wanted to make it a struct instead, we would have to redesign the API so that, instead of null, the Restore() method would be able to take either a Nullable<Memento>, some Option<Memento> type (.NET doesn’t have a built-in option type yet), or a memento possessing some easily identifiable trait (e.g., a balance of int.MinValue).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.91.44