Memento
is a behavioral design pattern that allows to save and restore an object's state without breaking encapsulation. Our implementation will be slightly different than the classic one, which splits the pattern into three components: memento
, caretaker
and originator
. Caretaker
will be responsible for saving and restoring a memento
object using a concrete implementation of storage and memento
will be represented as a protocol that defines a contract for all types that should have saving/restoring capabilities.
We start off from declaring Memento
protocol:
protocol Memento {
// MARK: - Properties
var data: [String : Any] { get }
// MARK: - Initializers
init?(data: [String : Any])
}
The protocol defines a read-only property for dictionary can be store Any
type for a String
key. The dictionary will be used to store and restore the internal state of a particular type. Then we defined a required, failable initializer that accepts a dictionary as an input parameter. The initializer will be used by the Caretaker
instance to restore an object.
Next, we implement a protocol called Caretaker
. The protocol will be responsible for declaring contract for all kinds of concrete mechanisms that will actually save and restore memento objects.
protocol Caretaker {
// MARK: - Properties
var states: [String : String] { get }
// MARK: - Methods
mutating func save<T: Memento>(memento: T, for stateName: String)
func restore<T: Memento>(state: String) -> T?
mutating func delete(state: String)
}
The states
property that is a dictionary of types String
and String
for keys and values, that will be internally holding states, in order to be able to re-use or reference them at run-time. As an API
the protocol defines three
methods for saving a memento
for a given state, restoring from state and deleting the state of a memento
object.
Let's implement a concrete Caretaker
called PropertyListCaretaker
that is built around UserDefaults
mechanism:
@dynamicMemberLookup
struct PropertyListCaretaker: Caretaker {
// MARK: - Private properties
private let standardDefaults = UserDefaults.standard
private static let STATES_KEY = "KEYS FOR STATES"
// MARK: - Initializers
init() {
if let states = standardDefaults.object(forKey: PropertyListCaretaker.STATES_KEY) as? [String : String] {
print("restored state keys: ", states)
self.states = states
}
}
// MARK: - Conformance to Caretaker protocol
var states: [String : String] = [:] {
didSet {
standardDefaults.set(states, forKey: PropertyListCaretaker.STATES_KEY)
}
}
mutating func save<T: Memento>(memento: T, for stateName: String) {
states.updateValue(stateName, forKey: stateName)
standardDefaults.set(memento.data, forKey: stateName)
}
func restore<T: Memento>(state: String) -> T? {
guard let data = standardDefaults.object(forKey: state) as? [String : Any] else {
return nil
}
let memento = T(data: data)
return memento
}
mutating func delete(state: String) {
states.removeValue(forKey: state)
standardDefaults.removeObject(forKey: state)
}
// MARK: - Dynamic Member Lookup Subscripts
subscript<T: Memento>(dynamicMember input: String) -> T? {
return restore(state: input)
}
}
The implementation is based on struct
. We defined a private UserDefaults
property for convenient re-use and added conformance for Caretaker
protocol. The implementation is pretty straightforward, we used the built-in API
for persistence. The interesting thing here is a relatively new feature of Swift
called dynamic member lookup
. We will use it to syntactically make the code more clear, however you may skip it, since it's not mandatory.
The final part of our implementation is to create a couple of classes that will conform to Memento
protocol. Let's implement a User
and an Animal
classes:
class User: Memento {
var name: String
var age: Int
var address: String
var data: [String : Any] {
return ["name" : name, "age" : age, "address" : address]
}
init(name: String, age: Int, address: String) {
self.name = name
self.age = age
self.address = address
}
required init?(data: [String : Any]) {
guard let mName = data["name"] as? String, let mAge = data["age"] as? Int, let mAddress = data["address"] as? String else {
return nil
}
name = mName
age = mAge
address = mAddress
}
}
class Animal: Memento {
var name: String
var age: Int
var data: [String : Any] {
return ["name" : name, "age" : age]
}
init(name: String, age: Int) {
self.name = name
self.age = age
}
required init?(data: [String : Any]) {
guard let mName = data["name"] as? String, let mAge = data["age"] as? Int else {
return nil
}
name = mName
age = mAge
}
}
The approach that is used to save and restore the state using the dictionary is pretty similar to the one used with NSCoding
protocol: we save the properties for the specified keys and then in the required, failable initializer we restore them. Futher it can be improved with more type-safe keys, rather than using String
literals.
The usage of the Memento
pattern is all about creating objects, saving states by using a concrete Caretaker
and later on restoring a state by using one of the String
keys.
// A new user - John
var user = User(name: "John", age: 26, address: "New Ave, 456")
// A new animal - Monkey
var animal = Animal(name: "Monkey", age: 8)
// The initial states of the objects are:
// name: John, age: 26, address: New Ave, 456
// name: Monkey, age: 8
var caretaker = PropertyListCaretaker()
// We save the default states of the user and animal
caretaker.save(memento: user, for: "defaultUser")
caretaker.save(memento: animal, for: "defaultAnimal")
// Then we change the age of the user and the animal
user.age = 32
animal.age = 10
// And save the states by using new state-keys
caretaker.save(memento: user, for: "defaultUser01")
caretaker.save(memento: animal, for: "defaultAnimal01")
// Then we change the names to be able to check if the pattern works correctly
user.name = "Alex"
animal.name = "Cat"
// As a result we have the following states of the objects:
// name: Alex, age: 32, address: New Ave, 456
// name: Cat, age: 10
In the presented snippet of code, we created a user and an animal. Then we created a PropertyListCaretaker
instance and saved the states of the memento
objects. Then we changed the ages of the objects and again saved then by using different state-keys. Finally, we changed the names of the memento
objects to be able to verify that we actually are able to save and restore objects using various state-keys.
if let restoredUser = caretaker.defaultUser as User? {
// name: John, age: 26, address: New Ave, 456
}
if let restoredUser = caretaker.defaultUser01 as User? {
// name: John, age: 32, address: New Ave, 456
}
if let restoredAnimal = caretaker.defaultAnimal as Animal? {
// name: Monkey, age: 8
}
if let restoredAnimal = caretaker.defaultAnimal02 as Animal? {
// name: Cat, age: 10
}
The first saved state for the state-key named defaultUser
is correct! As well as the remaining states. We are able to restart our macOS
or iOS
application and be able to restore any object that conforms to the Memento
protocol to a certain state.
Memento
pattern is quite useful in many cases: when implementing undo manager, also to save objects when a user closes an app, so they can be restored when the app is launched again. These are just a few examples, when there are many more practical applications. The presented pattern's implementation can be further improved in order to be used in production code. For example the part that is responsible for caretaking of the memento
objects, can be implemented using more reliable persistence approach. For example, in addition to UserDefaults
caretaker
you can implement CoreData
-based caretaker
that is more reliable and allows much more than just storing type's properties.