What is the Memento Design Pattern?
The Memento Design Pattern is a behavioral pattern, used to save an object’s state (its property values) to allow for restoring the object’s properties to their previous values (rollback or undo changes).
There are three components for the Memento Design Pattern: the Originator (class to maintain state history for), the Memento (holds a “snapshot” of the state), and the Caretaker (manages saving and restoring mementos).
Code Sample for Memento Design Pattern in C#
Here’s how to use a memento to track multiple snapshots of a Customer object’s property values.
In this example, I’ll have the Caretaker functions inside the Originator classes. However, you could create a separate Caretaker class to manage the Mementos.
namespace DesignPatternsCSharpNet6.Memento; public class Customer { // Save a list of memento objects, // to allow for multiple "snapshots" of the Customer object. private readonly List<CustomerMemento> _customerMementos = new List<CustomerMemento>(); public int ID { get; } public string Name { get; set; } public string Address { get; set; } public string City { get; set; } public string StateProvince { get; set; } public string PostalCode { get; set; } public bool IsDirty { get { if (_customerMementos.Count == 0) { return false; } return Name != _customerMementos.First().Name || Address != _customerMementos.First().Address || City != _customerMementos.First().City || StateProvince != _customerMementos.First().StateProvince || PostalCode != _customerMementos.First().PostalCode; } } public Customer(int id, string name, string address, string city, string stateProvince, string postalCode) { ID = id; Name = name; Address = address; City = city; StateProvince = stateProvince; PostalCode = postalCode; // Save the originally-passed values to the memento list. SaveCurrentStateToMemento(); } // The "Caretaker" functions public void SaveCurrentStateToMemento() { _customerMementos.Add(new CustomerMemento(this)); } public void RevertToOriginalValues() { // Get the first memento, if there is one (there always should be at least one). var firstMemento = _customerMementos.FirstOrDefault(); // Check for null, just to be safe. if (firstMemento != null) { SetPropertyValuesFromMemento(firstMemento); // Remove all the mementos, except for the first one. if (_customerMementos.Count > 1) { _customerMementos.RemoveRange(1, _customerMementos.Count - 1); } } } public void RevertToPreviousValues() { // Get the last memento, if there is one (there always should be at least one). var lastMemento = _customerMementos.LastOrDefault(); // Check for null, just to be safe. if (lastMemento != null) { SetPropertyValuesFromMemento(lastMemento); // Remove the last memento, unless it's the first one. if (lastMemento != _customerMementos.First()) { _customerMementos.Remove(lastMemento); } } } private void SetPropertyValuesFromMemento(CustomerMemento memento) { Name = memento.Name; Address = memento.Address; City = memento.City; StateProvince = memento.StateProvince; PostalCode = memento.PostalCode; } // The "Memento" class private class CustomerMemento { public string Name { get; } public string Address { get; } public string City { get; } public string StateProvince { get; } public string PostalCode { get; } public CustomerMemento(Customer customer) { Name = customer.Name; Address = customer.Address; City = customer.City; StateProvince = customer.StateProvince; PostalCode = customer.PostalCode; } } }
This is a typical simple Customer class, with its properties on lines 10-15.
I added a Boolean IsDirty property, to check if the object’s current property values are different from the values it was instantiated with. This isn’t required for the Memento pattern, but it is a nice capability you’ll have if your class has mementos.
When a Customer object is instantiated, the constructor calls SaveCurrentStateToMemento (line 45).
The SaveCurrentStateToMemento function creates a new CustomerMemento object, with the Customer object’s current property values, and adds it to the _customerMementos class-level variable.
RevertToOriginalValues gets the first CustomerMemento and reset the Customer’s properties to that CustomerMemento’s values. Then, it removes all other CustomerMementos, other than the first one that holds the original values the Customer classes was instantiated with.
RevertToPreviousValues will get the most recent CustomerMemento from the _customerMementos variable, reset the Customer object’s properties to that CustomerMemento’s values, and remove that memento from the list – unless it’s the first one.
Both functions that revert the changes call the SetPropertyValuesFromMemento function (lines 90-97) to reset the property values to the ones from the selected CustomerMemento.
With this technique, you can add an “undo” button to your program, and let the users go back through previous changes. You just need to decide when you want to call SaveCurrentStateToMemento, to create the different snapshots of the Customer object’s state.
The CustomerMemento class accepts a Customer object in its constructor and saves the Customer object’s property values to its properties – except the Id property. That’s because the Customer Id property is read-only. It only gets populated from the constructor. So, it will never change – which means we don’t need to track its history in the CustomerMemento class. Plus, the Id property does not have a setter, so we couldn’t change it anyway.
Notice that the CustomerMemento class is a private class, defined inside the Customer class. That means the CustomerMemento class is only visible inside the Customer class – just like a private variable or method.
Normally, I don’t like declaring more than one class inside a file. However, CustomerMemento will never be used outside of the Customer class. So, this is one of the rare exceptions to the rule of “one class per file”.
To me, this follows the OOP principles of “encapsulation” and “information hiding”. Nothing outside the Customer class knows how the CustomerMementos are created and stored. All the memento logic is hidden behind the SaveCurrentStateToMemento, RevertToOriginalValues, and RevertToPreviousValues functions.
You could put CustomerMemento in its own file, and you could move the Caretaker functionality to a separate class, if you prefer.
Using the Memento design pattern
To see how to use mementos with the Customer class, to roll back changes, here’s a test class that instantiates a Customer object, modifies the Address property several times, and reverts those changes.
using DesignPatternsCSharpNet6.Memento; namespace Test.DesignPatternsCSharpNet6.MementoPattern; public class TestMementoPattern { private const string ORIGINAL_ADDRESS = "1234 Main Street"; private const string CHANGED_ADDRESS_1 = "4321 Elm Street"; private const string CHANGED_ADDRESS_2 = "1111 Pine Road"; private const string CHANGED_ADDRESS_3 = "5555 5th Avenue"; [Fact] public void Test_IsDirtyAndRevertWork() { Customer customer = new Customer(1, "ABC", ORIGINAL_ADDRESS, "Houston", "TX", "77777"); // Should not be "dirty", because the initial values have not changed. Assert.False(customer.IsDirty); Assert.Equal(ORIGINAL_ADDRESS, customer.Address); customer.Address = CHANGED_ADDRESS_1; customer.SaveCurrentStateToMemento(); // Should be "dirty". Assert.True(customer.IsDirty); Assert.Equal(CHANGED_ADDRESS_1, customer.Address); customer.Address = CHANGED_ADDRESS_2; customer.SaveCurrentStateToMemento(); // Should be "dirty". Assert.True(customer.IsDirty); Assert.Equal(CHANGED_ADDRESS_2, customer.Address); customer.Address = CHANGED_ADDRESS_3; // Should be "dirty". Assert.True(customer.IsDirty); Assert.Equal(CHANGED_ADDRESS_3, customer.Address); // Revert the current change, to latest saved memento. customer.RevertToPreviousValues(); // Should be "dirty". Assert.True(customer.IsDirty); Assert.Equal(CHANGED_ADDRESS_2, customer.Address); // Go back to original values. customer.RevertToOriginalValues(); // Should not be "dirty" after reverting values. Assert.False(customer.IsDirty); Assert.Equal(ORIGINAL_ADDRESS, customer.Address); } }
This is a nice technique for programs that involve a lot of user input, where the user may want to undo their changes.
For some programs, you may also be able to use a cloning library to create copies of the object, instead of using the Memento class.
Just make sure, if the object has properties that are other classes, you use a deep-cloning library. Deep-cloning creates new objects for the properties that are objects (and their properties that are objects). A “shallow copy” will hold references to the original object(s). So, changing a property value in the object may also change the related property value in the clone/memento.