備忘錄模式(memento pattern)用于保存對象歷史狀態(tài),以便后續(xù)可以恢復(fù)至任一狀態(tài)。Memento pattern 是二十三種著名的 Gof design patterns 設(shè)計模式之一,屬于 Behavioral Patterns。Memento pattern 由 Noah Thompson 和 Dr.Drew Clinkenbeard 為惠普產(chǎn)品創(chuàng)建。
Memento pattern 有以下三個部分:

- Originator:要保存或恢復(fù)的對象。
- Memento:負(fù)責(zé)存儲 originator 對象的內(nèi)部狀態(tài)。
- CareTaker:請求 originator 保存對象,并得到 memento 響應(yīng)。其負(fù)責(zé)持久化 memento,或把 memento 提供給 originator 以恢復(fù)到指定狀態(tài)。
雖不是嚴(yán)格要求,但 iOS app 通常使用Encoder將 originator 的狀態(tài)編碼為 memento,使用Decoder解碼 memento 還原給 originator。這允許編碼、解碼邏輯在 originator 之間復(fù)用。例如,JSONEncoder和JSONDecoder能夠把對象編碼為 JSON 格式數(shù)據(jù),并解碼還原。
1. Memento pattern 解決了哪些問題
- 對象的內(nèi)部狀態(tài)需要保存到外部,以便稍后可以將對象恢復(fù)至此狀態(tài)。
- 不得違反對象的封裝(encapsulation)。
問題在于已經(jīng)封裝好的對象,其數(shù)據(jù)結(jié)構(gòu)隱藏在對象內(nèi)部,并且不能從對象外部訪問。
2. Memento pattern 提供了哪些解決方案
使用 originator 負(fù)責(zé):
- 保存對象內(nèi)部狀態(tài)為 memento
- 使用之前保存的 memento 恢復(fù)為之前的狀態(tài)
只有創(chuàng)建 memento 的 originator 才能夠訪問該 memento。CareTaker 可以向 originator 請求當(dāng)前狀態(tài)的 memento,也可以把 memento 傳遞給 originator 以便恢復(fù)到指定狀態(tài)。這樣就可以在不破壞其封裝的前提下實現(xiàn)保存、恢復(fù)需求。
例如,可以使用 memento pattern 實現(xiàn)保存游戲進度。其中,originator 是游戲狀態(tài)(如等級、健康狀況、生命值等),memento 是保存的數(shù)據(jù),caretaker 是游戲系統(tǒng)。
你也可以保存一個 memento 的數(shù)組,代表之前的游戲進度。這樣也可以實現(xiàn)IDE或圖形軟件中的撤銷、重做堆棧等功能。
3. Demo
在這個示例中將會創(chuàng)建一個簡單的游戲系統(tǒng)。
3.1 Originator
首先,定義 originator 部分。在 playground 添加以下代碼:
import Foundation
// MARK: - Originator
public class Game: Codable {
public class State: Codable {
public var attempsRemaining: Int = 5
public var level: Int = 1
public var score: Int = 0
}
public var state = State()
public func rackUpMassivePoints() {
state.score += 8008
}
public func monstersEatPlayer() {
state.attempsRemaining -= 1
}
}
這里定義了一個Game對象,其內(nèi)部狀態(tài)記錄了游戲?qū)傩?、方法。同時,Game和State遵守Codable。
Apple 在 Swift 4 中增加了Codable。任何遵守Codable的對象都可以轉(zhuǎn)換為外部存儲,或從外部存儲讀取。本質(zhì)上,它是一種可以自我保存、恢復(fù)的類型。這正是 originator 需要實現(xiàn)的。
由于Game和State的屬性已經(jīng)遵守Codable協(xié)議,編譯器會自動生成所需協(xié)議方法。Swift 提供的String、Int、Double和大部分其他類型均已遵守Codable。
事實上,Codable是Encodable和Decodable的別名 typealias:
typealias Codable = Decodable & Encodable
可編碼類型可以通過編碼器編碼為外部表示。外部表示的類型取決所使用的編碼器。Foundation提供了幾個默認(rèn)的編碼器,包括用于將對象轉(zhuǎn)換為 JSON 格式的JSONEncoder。
可解碼的類型可以通過解碼器從外部表示轉(zhuǎn)換。Foundation為解碼器提供了解決方案,包括用于從 JSON 數(shù)據(jù)轉(zhuǎn)換對象的JSONDecoder。
3.2 Memento
下一步聲明Memento,添加以下代碼:
// MARK: - Memento
typealias GameMemento = Data
事實上,不需要聲明上述類型。這里只是說明GameMemento是要保存的數(shù)據(jù)。這將由Encoder在保存時生成,并由Decoder在恢復(fù)時使用。
3.3 CareTaker
接下來,需要聲明CareTaker,如下所示:
// MARK: - CareTaker
public class GameSystem {
public static let decoder = JSONDecoder()
public static let encoder = JSONEncoder()
// 1
public static func save<T: Codable>(_ object: T, with title: String) throws {
do {
let url = createDocumentURL(withTitle: title)
let data = try encoder.encode(object)
try data.write(to: url, options: .atomic)
} catch (let error) {
dump(error)
throw error
}
}
// 2
public static func retrieve<T: Codable>(_ type: T.Type, with title: String) throws -> T {
let url = createDocumentURL(withTitle: title)
return try retrieve(T.self, from: url)
}
public static func retrieve<T: Codable>(_ type: T.Type, from url: URL) throws -> T{
do {
let data = try Data(contentsOf: url)
return try decoder.decode(T.self, from: data)
} catch (let error) {
dump(error)
throw error
}
}
public static func createDocumentURL(withTitle title: String) -> URL {
let fileManager = FileManager.default
let url = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
return url.appendingPathComponent(title).appendingPathExtension("json")
}
}
-
save(_:with title:)封裝了保存邏輯。首先,使用JSONEncoder編碼傳入的game參數(shù)。這一操作可能拋出異常,必須使用try關(guān)鍵字修飾。最后,把 data 以 title 名稱保存到.documentDirectory目錄。 -
retrieve(_:from url:)封裝了恢復(fù)邏輯。首先,讀取.documentDirectory目錄中 title 文件,最后使用JSONDecoder恢復(fù)Game對象。讀取、恢復(fù)任一操作失敗,均會拋出異常;讀取、恢復(fù)操作都成功時,返回恢復(fù)的Game對象。
3.4 具體應(yīng)用
在 playground 底部添加以下代碼:
// MARK: - Example
var game = Game()
game.monstersEatPlayer()
game.rackUpMassivePoints()
這里模擬玩游戲,玩家被怪物吃掉后卷土重來,并獲得大量積分。
添加以下代碼:
// Save Game
try? GameSystem.save(game, with: "Best Game Ever")
這里玩家把當(dāng)前進度保存,以便后續(xù)繼續(xù)或者恢復(fù)。
當(dāng)然,玩家仍然可以開始其他關(guān)卡游戲。如下所示:
// New Game
game = Game()
game.state.score = 200
dump(game)
這里創(chuàng)建了一個新的實例,輸出game對象??刂婆_輸出如下:
? __lldb_expr_3.Game #0
? state: __lldb_expr_3.Game.State #1
- attempsRemaining: 5
- level: 1
- score: 200
玩家也可以恢復(fù)之前保存的Game,添加以下代碼:
// Load Game
game = try! GameSystem.retrieve(Game.self, with: "Best Game Ever")
dump(game)
這里加載原來保存的Game,并輸出game對象:
? __lldb_expr_3.Game #0
? state: __lldb_expr_3.Game.State #1
- attempsRemaining: 4
- level: 1
- score: 8008
編碼和解碼都可能會觸發(fā)異常,因此,添加、移除
Codable屬性時需要小心。如果解包時使用try!,在 app 沒有此數(shù)據(jù)時會觸發(fā)異常。為解決這個問題,應(yīng)避免使用try!,只在確定存在時使用。
總結(jié)
以下是 Memento Design Pattern 的關(guān)鍵點:
- Memento pattern 允許保存和恢復(fù)對象。其涉及三部分:originator、memento、caretaker。
- Originator是要保存的對象,memento 是保存的狀態(tài),caretaker 保存、恢復(fù)memento對象。
- iOS 提供的
Encoder將 originator 的狀態(tài)編碼為 memento,使用Decoder解碼 memento 還原給 originator。編碼和解碼邏輯在 originator 間復(fù)用。
Demo名稱:MementoPattern
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/MementoPattern
參考資料: