Memento is a behavioral design pattern that allows making snapshots of an object’s state and restoring it in future.
The Memento doesn’t compromise the internal structure of the object it works with, as well as data kept inside the snapshots.
Conceptual Example
This example illustrates the structure of the Memento design pattern and focuses on the following questions:
What classes does it consist of?
What roles do these classes play?
In what way the elements of the pattern are related?
After learning about the pattern’s structure it’ll be easier for you to grasp the following example, based on a real-world Swift use case.
Example.swift: Conceptual example
import XCTest
/// The Originator holds some important state that may change over time. It also
/// defines a method for saving the state inside a memento and another method
/// for restoring the state from it.
class Originator {
/// For the sake of simplicity, the originator's state is stored inside a
/// single variable.
private var state: String
init(state: String) {
self.state = state
print("Originator: My initial state is: \(state)")
}
/// The Originator's business logic may affect its internal state.
/// Therefore, the client should backup the state before launching methods
/// of the business logic via the save() method.
func doSomething() {
print("Originator: I'm doing something important.")
state = generateRandomString()
print("Originator: and my state has changed to: \(state)")
}
private func generateRandomString() -> String {
return String(UUID().uuidString.suffix(4))
}
/// Saves the current state inside a memento.
func save() -> Memento {
return ConcreteMemento(state: state)
}
/// Restores the Originator's state from a memento object.
func restore(memento: Memento) {
guard let memento = memento as? ConcreteMemento else { return }
self.state = memento.state
print("Originator: My state has changed to: \(state)")
}
}
/// The Memento interface provides a way to retrieve the memento's metadata,
/// such as creation date or name. However, it doesn't expose the Originator's
/// state.
protocol Memento {
var name: String { get }
var date: Date { get }
}
/// The Concrete Memento contains the infrastructure for storing the
/// Originator's state.
class ConcreteMemento: Memento {
/// The Originator uses this method when restoring its state.
private(set) var state: String
private(set) var date: Date
init(state: String) {
self.state = state
self.date = Date()
}
/// The rest of the methods are used by the Caretaker to display metadata.
var name: String { return state + " " + date.description.suffix(14).prefix(8) }
}
/// The Caretaker doesn't depend on the Concrete Memento class. Therefore, it
/// doesn't have access to the originator's state, stored inside the memento. It
/// works with all mementos via the base Memento interface.
class Caretaker {
private lazy var mementos = [Memento]()
private var originator: Originator
init(originator: Originator) {
self.originator = originator
}
func backup() {
print("\nCaretaker: Saving Originator's state...\n")
mementos.append(originator.save())
}
func undo() {
guard !mementos.isEmpty else { return }
let removedMemento = mementos.removeLast()
print("Caretaker: Restoring state to: " + removedMemento.name)
originator.restore(memento: removedMemento)
}
func showHistory() {
print("Caretaker: Here's the list of mementos:\n")
mementos.forEach({ print($0.name) })
}
}
/// Let's see how it all works together.
class MementoConceptual: XCTestCase {
func testMementoConceptual() {
let originator = Originator(state: "Super-duper-super-puper-super.")
let caretaker = Caretaker(originator: originator)
caretaker.backup()
originator.doSomething()
caretaker.backup()
originator.doSomething()
caretaker.backup()
originator.doSomething()
print("\n")
caretaker.showHistory()
print("\nClient: Now, let's rollback!\n\n")
caretaker.undo()
print("\nClient: Once more!\n\n")
caretaker.undo()
}
}
Output.txt: Execution result
Originator: My initial state is: Super-duper-super-puper-super.
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: 1923
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: 74FB
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: 3681
Caretaker: Here's the list of mementos:
Super-duper-super-puper-super. 11:45:44
1923 11:45:44
74FB 11:45:44
Client: Now, let's rollback!
Caretaker: Restoring state to: 74FB 11:45:44
Originator: My state has changed to: 74FB
Client: Once more!
Caretaker: Restoring state to: 1923 11:45:44
Originator: My state has changed to: 1923
Real World Example
Example.swift: Real world example
import XCTest
class MementoRealWorld: XCTestCase {
/// State and Command are often used together when the previous state of the
/// object should be restored in case of failure of some operation.
///
/// Note: UndoManager can be used as an alternative.
func test() {
let textView = UITextView()
let undoStack = UndoStack(textView)
textView.text = "First Change"
undoStack.save()
textView.text = "Second Change"
undoStack.save()
textView.text = (textView.text ?? "") + " & Third Change"
textView.textColor = .red
undoStack.save()
print(undoStack)
print("Client: Perform Undo operation 2 times\n")
undoStack.undo()
undoStack.undo()
print(undoStack)
}
}
class UndoStack: CustomStringConvertible {
private lazy var mementos = [Memento]()
private let textView: UITextView
init(_ textView: UITextView) {
self.textView = textView
}
func save() {
mementos.append(textView.memento)
}
func undo() {
guard !mementos.isEmpty else { return }
textView.restore(with: mementos.removeLast())
}
var description: String {
return mementos.reduce("", { $0 + $1.description })
}
}
protocol Memento: CustomStringConvertible {
var text: String { get }
var date: Date { get }
}
extension UITextView {
var memento: Memento {
return TextViewMemento(text: text,
textColor: textColor,
selectedRange: selectedRange)
}
func restore(with memento: Memento) {
guard let textViewMemento = memento as? TextViewMemento else { return }
text = textViewMemento.text
textColor = textViewMemento.textColor
selectedRange = textViewMemento.selectedRange
}
struct TextViewMemento: Memento {
let text: String
let date = Date()
let textColor: UIColor?
let selectedRange: NSRange
var description: String {
let time = Calendar.current.dateComponents([.hour, .minute, .second, .nanosecond],
from: date)
let color = String(describing: textColor)
return "Text: \(text)\n" + "Date: \(time.description)\n"
+ "Color: \(color)\n" + "Range: \(selectedRange)\n\n"
}
}
}
Output.txt: Execution result
Text: First Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 821737051 isLeapMonth: false
Color: nil
Range: {12, 0}
Text: Second Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 826483011 isLeapMonth: false
Color: nil
Range: {13, 0}
Text: Second Change & Third Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 829187035 isLeapMonth: false
Color: Optional(UIExtendedSRGBColorSpace 1 0 0 1)
Range: {28, 0}
Client: Perform Undo operation 2 times
Text: First Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 821737051 isLeapMonth: false
Color: nil
Range: {12, 0}