備忘錄模式 Memento Pattern

備忘錄模式(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 有以下三個部分:

MementoPatternUML.png
  1. Originator:要保存或恢復(fù)的對象。
  2. Memento:負(fù)責(zé)存儲 originator 對象的內(nèi)部狀態(tài)。
  3. 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ù)用。例如,JSONEncoderJSONDecoder能夠把對象編碼為 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ū)傩?、方法。同時,GameState遵守Codable。

Apple 在 Swift 4 中增加了Codable。任何遵守Codable的對象都可以轉(zhuǎn)換為外部存儲,或從外部存儲讀取。本質(zhì)上,它是一種可以自我保存、恢復(fù)的類型。這正是 originator 需要實現(xiàn)的。

由于GameState的屬性已經(jīng)遵守Codable協(xié)議,編譯器會自動生成所需協(xié)議方法。Swift 提供的StringInt、Double和大部分其他類型均已遵守Codable

事實上,CodableEncodableDecodable的別名 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")
    }
}
  1. save(_:with title:)封裝了保存邏輯。首先,使用JSONEncoder編碼傳入的game參數(shù)。這一操作可能拋出異常,必須使用try關(guān)鍵字修飾。最后,把 data 以 title 名稱保存到.documentDirectory目錄。
  2. 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

參考資料:

  1. Memento pattern
  2. Design-Patterns-In-Swift

歡迎更多指正:https://github.com/pro648/tips/wiki

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容