Swift 中的設計模式 #2 觀察者模式與備忘錄模式

SwiftGG翻譯組發表於2019-02-28

作者:Andrew Jaffee,原文連結,原文日期:2018-08-06
譯者:jojotov;校對:Forelaxpmst;定稿:Forelax

本次教程是 AppCoda 上週開啟 的設計模式系列的第二期。在軟體設計領域的四位大師級人物(GoF,又稱“四人幫”或“Gang of Four”) —— Erich Gamma, Richard Helm, Ralph Johnson 和 John Vlissides 所著的 《設計模式:可複用物件導向軟體的基礎》一書中,首次對軟體設計中總共 23 種設計模式進行了定義和歸類,並對它們作了專業闡述。今天,我們將聚焦於其中兩個行為型設計模式 —— “觀察者模式” 和 “備忘錄模式”。

軟體開發領域致力於對真實世界的場景進行建模,並期望能創造出一系列工具來提高人類對這些場景的理解與體驗。與 10 年前相比,一些類似銀行應用和輔助購物應用(如亞馬遜或 eBay 的 iOS 客戶端應用)的財務類工具的出現,無疑讓顧客們的生活變得更加簡單。當我們回顧軟體開發的發展歷程,定會感嘆我們在軟體開發領域上走過了漫長而又成功的道路。如今,軟體應用的功能普遍變得強大且易用,但對於開發者來說,開發這些軟體卻變得 越來越複雜

開發者們為了管理軟體的複雜性,創造了一系列的最佳實踐 —— 例如物件導向程式設計、面向協議程式設計、值語義、區域性推理(Local Reasoning)、把大塊的程式碼切分成一系列小塊的程式碼並附上友好的介面(如 Swift 中的擴充套件)、語法糖等。但在眾多的最佳實踐之中,有一個非常重要且值得我們關注的最佳實踐並沒有在上文中提及 —— 那就是設計模式的使用。

設計模式

對於開發者來說,設計模式是管理程式碼複雜度問題的一個極其重要的工具。我們理解設計模式最好的辦法,就是把設計模式概念化為 —— 有固定模版的通用技術,每個設計模式都旨在解決相應的一個反覆出現且易於辨別的特定問題。你可以把設計模式看作是一系列最佳實踐的集合,它們可以用於一些經常出現的編碼場景:例如如何利用一系列有關聯的物件建立出新的物件,並且不需要去理解原本那一系列物件中“又臭又長”的程式碼實現。設計模式最重要的意義是其可以應用於那些常見的場景。同時,由於設計模式都是已經創造出來的固定模式,拿來即用的特質令它具有很高的易用性。為了能更好的理解設計模式,我們來看一個例子:

設計模式並不能解決一些非常具體的問題。例如 “如何在 Swift 中遍歷一個包含 11 個整型(Int)的 陣列”之類的的問題。我們從一個例子來更好地理解為什麼設計模式不能解決此類具體問題 —— GoF 定義了迭代器模式,為“便捷地遍歷集合的所有元素,而不需要知道集合中元素的型別” 的問題提出通用的解決方案,。因此,我們不能單純地把設計模式當作某種語言的程式碼,它只是用於解決通用軟體開發場景的規則和指引。

我曾經在 AppCoda 中討論過 “Model-View-ViewModel” 或 “MVVM” 設計模式 —— 當然也少不了那個 Apple 和 眾多 iOS 開發者們長期喜愛著的經典設計模式 “Model-View-Controller” 或 “MVC”

這兩種設計模式通常來說會應用於整個應用層面。MVVM 和 MVC 可以看作是架構層面上的設計模式,它們主要的作用可以簡單分成四個方面:隔離使用者介面(UI)與應用的資料;隔離使用者介面(UI)與負責展示邏輯的程式碼;隔離應用的資料與核心資料處理邏輯;隔離應用的資料與業務邏輯。GoF 的設計模式都具有特殊性,它們都旨在解決一些在應用的程式碼庫中較為特別的問題。你可能會在開發一個應用的時候用到好幾個 GoF 的設計模式。同時你需要記得設計模式並不只是一些具體的程式碼例項,它們解決的是一些更抽象層面上的問題(希望你沒忘記上面的迭代器例子)。除此之外我還想提及一個沒有在 GoF 列出的 23 個設計模式之中,但卻是設計模式的 典型例子 —— 代理模式。

雖然 GoF 關於設計模式的書已經被眾多開發者視為聖經般的存在,但仍存在一些對其批判的聲音。我們會在本文結尾的部分討論這個問題。

設計模式類別

GoF 把他們提出的 23 種設計模式整理到了 3 種大的類別中:“建立型模式”、“結構型模式”以及“行為型模式”。本次的教程會討論行為型模式類別中的兩種設計模式。行為型模式的主要作用是對類和結構體(參與者)的行為賦予安全性、合理性,以及定義一些統一的規則、統一的的形式和最佳實踐。對於整個應用中的參與者,我們都希望有一個良好的、統一的、並且可預測的行為。同時,我們不僅希望參與者本身擁有良好的行為,也希望不同的參與者之間的互動/通訊可以擁有良好的行為。對於參與者的行為評估,其時機應該在編譯之前以及編譯時 —— 我通常把這段時間稱之為“設計時間”,以及在執行時 —— 此時我們會有大量的類和結構體的例項在各司其職或與其他例項互動/通訊。由於例項間的通訊會導致軟體複雜度的增加,因此制定一系列關於一致性、高效率和安全通訊的規則是極為重要的,但與此同時,在構建每個單獨的參與者時,這個概念不應以任何方式降低設計的質量。由於需要非常著重於行為,我們必須牢記一點 —— 在賦予參與者職責時必須使用一致的模式。

在高談闊論太多理論之前,讓我先說明一下本次教程中你會有哪些收穫,同時所有的相關實踐程式碼我都會用 Swift 來實現。在本次教程中,我們會了解到如何能夠通過一致地賦予職責來維持參與者的狀態。我們會了解到如何一致地賦予職責給一個參與者,讓他能夠傳送通知給其他觀察者。與之對應,我們也會了解到如何一致地賦予職責給觀察者們註冊通知。

當你討論設計模式時,你應該把一致性當成最顯而易見的基本概念。在 上週的推送 中,我們著重討論了一個概念:高複雜度(封裝)。你必須把這個概念當作中心思想牢記於心,因為它會隨著我們更深入地討論設計模式而出現地越發頻繁。舉個例子,物件導向(OOP)中的眾多類,可以在不需要開發者知道任何其內部實現的前提下,提供非常複雜、成熟且強大的功能。同樣, Swift 的 面向協議程式設計 也是一項對於控制複雜度來說極為重要的新技術。對開發者來說,想要管理好 複雜度 是一件異常困難的事情,但我們現在即將把這頭野獸馴服!

關於此教程的提醒

在這次的教程中,我決定把文字聚焦於對示例程式碼的解釋。我將對今天所要介紹的設計模式概念進行一些簡單明瞭的陳述,但同時為了能夠讓你更好地理解我所分享的技術,希望你可以認真看看程式碼和註釋。畢竟作為一名程式設計師,如果你只能談論程式碼而不能編寫程式碼,那你可能會在很多面試中失利 —— 因為你還不夠硬核。

你也許會留意到,我對與行為型設計模式的定義是遵循於蘋果的 文件規範:

傳統意義上來說,我們視一個類的例項為一個物件。雖然如此,但相對於其他語言而言,Swift 中的結構體和類在功能上非常相似,且本章節大部分內容描述了類或者結構體例項的功能。因此,我們使用更為通用的描述——例項。

在設計時,我把對類和結構體的任何引用都描述為“參與者”。

觀察者模式

在使用蘋果的移動裝置時,觀察者模式可能是貫穿整個應用使用過程的東西 —— 你應該在編寫 iOS 應用時也發現了這一點。在我生活的地方,每當下雨天的時候,包括我在內的很大一部分人都會收到 iPhone 上的通知。不管是鎖屏的狀態還是解鎖的狀態,只要下起雨來,你都會收到一條類似下面這樣的通知:

PushNotification demo

作為所有通知的源頭,蘋果會代表國家氣象局(National Weather Service)向成千上萬的 iPhone 使用者傳送(廣播)通知,提醒他們在其區域內是否有洪水災害的風險。更具體一點地在 iOS 應用層級上說,當某一個例項(也就是被觀察者)的狀態發生改變時,它會通知其他(不止一個)的被稱為觀察者的例項,告訴他們自己的某個狀態發生了變化。所有參與此次廣播通訊的例項都不需要知道除了自身以外的任何其他例項。這是一個關於 鬆耦合 的絕佳示例。

被觀察的例項(通常是一個重要的資源)會廣播關於自身狀態改變的通知給其他眾多觀察者例項。對這些狀態改變有興趣的觀察者必須通過訂閱來獲取關於狀態改變通知。

這次我們不得不說蘋果還是很靠譜的,iOS 已經內建了一個廣為人知的用於觀察者模式的特性:NotificationCenter。在這裡我不會對其作過多介紹,讀者可自行在 這裡 學習相關內容。

觀察者模式用例

我的觀察者模式示例專案展示了這種廣播型別的通訊是如何工作的,你可以在 Github 上找到它

假設我們有一個工具來監視網路連線狀況,並對已連線或未連線的狀態作出響應。這個工具我們可以稱之為廣播者。為了實現此工具,你需要一個參與者遵循我提供的 ObservedProtocol 協議。雖然我知道這麼做並不太符合蘋果的 iOS Human Interface Guideline 的建議,但我為了更好地演示觀察者模式,我需要以網路狀況作為僅有的一個關鍵資源。

假設現在有許多個不同的觀察者例項全都向被觀察的物件訂閱了關於網路連線狀況的通知,例如一個圖片下載類,一個通過 REST API 驗證使用者資格的登入業務例項,以及一個應用內瀏覽器。為了實現這些,你需要建立多個繼承於我提供的 Observer 抽象類(此基類同時遵循 ObserverProtocol 協議)的自定義子類。(我稍後會解釋為何我會把我關於觀察者的示例程式碼放在一個類中)。

為了實現我示例應用中的觀察者們,我建立了一個 NetworkConnectionHandler 類。當這個類的具體例項接收到 NetworkConnectionStatus.connected 通知時,這些例項會把幾個檢視變成綠色;當接收到 NetworkConnectionStatus.disconnected 通知時,會把檢視變成紅色。

下面是我的程式碼在 iPhone 8 Plus 裝置上執行的效果:

img

上面的執行過程中,Xcode 控制檯的輸出如下:

img

觀察者模式應用的示例程式碼

關於我上面所說的程式碼,你可以在專案中的 Observable.swift 檔案找到。每段程式碼我都加上了詳細的註釋。

import Foundation
 
import UIKit
 
// 定義通知名常量。
// 使用常量作為通知名,不要使用字串或者數字。
extension Notification.Name {
    
    static let networkConnection = Notification.Name("networkConnection")
    static let batteryStatus = Notification.Name("batteryStatus")
    static let locationChange = Notification.Name("locationChange")
 
}
 
// 定義網路狀態常量。
enum NetworkConnectionStatus: String {
    
    case connected
    case disconnected
    case connecting
    case disconnecting
    case error
    
}
 
// 定義 userInfo 中的 key 值。
enum StatusKey: String {
    case networkStatusKey
}
 
// 此協議定義了*觀察者*的基本結構。
// 觀察者即一些實體的集合,它們的操作嚴格依賴於其他實體的狀態。
// 遵循此協議的例項會向某些重要的實體/資源*訂閱*並*接收*通知。
protocol ObserverProtocol {
    
    var statusValue: String { get set }
    var statusKey: String { get }
    var notificationOfInterest: Notification.Name { get }
    func subscribe()
    func unsubscribe()
    func handleNotification()
    
}
 
// 此模版類抽象如何*訂閱*和*接受*重要實體/資源的通知的所有必要細節。
// 此類提供了一個鉤子方法(handleNotification()),
// 所有的子類可以通過此方法在接收到特定通知時進行各種需要的操作。
// 此類基為一個*抽象*類,並不會在編譯時被檢測,但這似乎是一個異常場景。
class Observer: ObserverProtocol {
    
    // 此變數與 notificationOfInterest 通知關聯。
    // 使用字串以儘可能滿足需要。
    var statusValue: String
    // 通知的 userInfo 中的 key 值,
    // 通過此 key 值讀取到特定的狀態值並儲存到 statusValue 變數。
    // 使用字串以儘可能滿足需要。
    let statusKey: String
    // 此類所註冊的通知名。
    let notificationOfInterest: Notification.Name

    // 通過傳入的通知名和需要觀察的狀態的 key 值進行初始化。
    // 初始化時會註冊/訂閱/監聽特定的通知並觀察特定的狀態。
    init(statusKey: StatusKey, notification: Notification.Name) {
        
        self.statusValue = "N/A"
        self.statusKey = statusKey.rawValue
        self.notificationOfInterest = notification
        
        subscribe()
    }
    
    // 向 NotificationCenter 註冊 self(this) 來接收所有儲存在 notificationOfInterest 中的通知。
    // 當接收到任意一個註冊的通知時,會呼叫 receiveNotification(_:) 方法。
    func subscribe() {
        NotificationCenter.default.addObserver(self, selector: #selector(receiveNotification(_:)), name: notificationOfInterest, object: nil)
    }
    
    // 在不需要監聽時登出所有已註冊的通知是一個不錯的做法,
    // 但這主要是由於歷史原因造成的,iOS 9.0 之後 OS 系統會自動做一些清理。
    func unsubscribe() {
        NotificationCenter.default.removeObserver(self, name: notificationOfInterest, object: nil)
    }
    
    // 在任意一個 notificationOfInterest 所定義的通知接收到時呼叫。
    // 在此方法中可以根據所觀察的重要資源的改變進行任意操作。
    // 此方法**必須有且僅有一個引數(NSNotification 例項)。**
    @objc func receiveNotification(_ notification: Notification) {
        
        if let userInfo = notification.userInfo, let status = userInfo[statusKey] as? String {
            
            statusValue = status
            handleNotification()
            
            print("Notification (notification.name) received; status: (status)")
            
        }
        
    } // receiveNotification 方法結束
    
    // **必須重寫此方法;且必須繼承此類**
    // 我使用了些"技巧"來讓此類達到抽象類的形式,因此你可以在子類中做其他任何事情而不需要關心關於 NotificationCenter 的細節。
    func handleNotification() {
        fatalError("ERROR: You must override the [handleNotification] method.")
    }
    
    // 析構時取消對 Notification 的關聯,此時已經不需要進行觀察了。
    deinit {
        print("Observer unsubscribing from notifications.")
        unsubscribe()
    }
    
} // Observer 類結束
 
// 一個具體觀察者的例子。
// 通常來說,會有一系列(許多?)的觀察者都會監聽一些單獨且重要的資源發出的通知。
// 需要注意此類已經簡化了實現,並且可以作為所有通知的 handler 的模板。
class NetworkConnectionHandler: Observer {
    
    var view: UIView
    
    // 你可以建立任意型別的構造器,只需要呼叫 super.init 並傳入合法且可以配合 NotificationCenter 使用的通知。
    init(view: UIView) {
        
        self.view = view
        
        super.init(statusKey: .networkStatusKey, notification: .networkConnection)
    }
    
    // **必須重寫此方法**
    // 此方法中可以加入任何處理通知的邏輯。
    override func handleNotification() {
        
        if statusValue == NetworkConnectionStatus.connected.rawValue {
            view.backgroundColor = UIColor.green
        }
        else {
            view.backgroundColor = UIColor.red
        }
        
    } // handleNotification() 結束
    
} // NetworkConnectionHandler 結束
 
// 一個被觀察者的模板。
// 通常被觀察者都是一些重要資源,在其自身某些狀態發生改變時會廣播通知給所有訂閱者。
protocol ObservedProtocol {
    var statusKey: StatusKey { get }
    var notification: Notification.Name { get }
    func notifyObservers(about changeTo: String) -> Void
}
 
// 在任意遵循 ObservedProtocol 示例的某些狀態發生改變時,會通知*所有*已訂閱的觀察者。
// **向所有訂閱者廣播**
extension ObservedProtocol {
 
    func notifyObservers(about changeTo: String) -> Void {
       NotificationCenter.default.post(name: notification, object: self, userInfo: [statusKey.rawValue : changeTo])
    }
    
} // ObservedProtocol 擴充套件結束
複製程式碼

我把大部分關於觀察者的通知處理邏輯放在了 ObserverProtocol 的擴充套件當中,並且這段邏輯會在一個 @objc 修飾的方法中執行(此方法同時會設定為通知的 #selector 的方法)。作為抽象類中的方法,相較於使用基於 block 的 addObserver(forName:object:queue:using:) 並把處理通知的閉包傳進去,使用 selector 可以讓這段通知處理程式碼顯得更加容易理解以及更加適合教學。

同時,我意識到 Swift 中並沒有關於抽象類的官方概念。因此,為了完成我解釋觀察者模式的教學目的,我強制使用者重寫 ObserverhandleNotification() 方法,以此來達到 “抽象類” 的形態。如此以來,你可以注入任意的處理邏輯,讓你的子類例項在接收到通知後有特定的行為。

下面我將展示示例專案中的 ViewController.swift 檔案,在這裡你可以看到剛剛討論過的 Obesever.swift 中的核心程式碼是如何使用的:

import UIKit
 
// 此 view controller 遵循 ObservedProtocol 協議,因此在*整個應用期間*
// 其可以通過 NotificationCenter 向*任意*有意接收的實體廣播通知。
// 可以看到這個類僅僅需要很少量的程式碼便可以實現通知的功能。
class ViewController: UIViewController, ObservedProtocol {
    
    @IBOutlet weak var topBox: UIView!
    @IBOutlet weak var middleBox: UIView!
    @IBOutlet weak var bottomBox: UIView!
    
    // Mock 一些負責觀察網路狀況的實體物件。
    var networkConnectionHandler0: NetworkConnectionHandler?
    var networkConnectionHandler1: NetworkConnectionHandler?
    var networkConnectionHandler2: NetworkConnectionHandler?
    
    // 遵循 ObservedProtocol 的兩個屬性。
    let statusKey: StatusKey = StatusKey.networkStatusKey
    let notification: Notification.Name = .networkConnection
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 初始化負責監聽的實體物件。
        networkConnectionHandler0 = NetworkConnectionHandler(view: topBox)
        networkConnectionHandler1 = NetworkConnectionHandler(view: middleBox)
        networkConnectionHandler2 = NetworkConnectionHandler(view: bottomBox)
    }
 
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    // Mock 一個可以改變狀態的重要資源。
    // 此處模擬此 ViewController 可以檢測網路連線狀況,
    // 當網路可以連線或者網路連線丟失時,通知所有有興趣的監聽者。
    @IBAction func switchChanged(_ sender: Any) {
        
        let swtich:UISwitch = sender as! UISwitch
        
        if swtich.isOn {
            notifyObservers(about: NetworkConnectionStatus.connected.rawValue)
        }
        else {
            notifyObservers(about: NetworkConnectionStatus.disconnected.rawValue)
        }
        
    } // switchChanged 函式結束
    
} // ViewController 類結束
複製程式碼

備忘錄模式

大部分 iOS 開發者對備忘錄模式都很熟悉。回憶一下 iOS 中十分便利的 歸檔和序列化功能,讓你能夠 “在物件和基本資料型別在 plist、JSON 和其他二進位制形式之間自由轉換”。再回憶一下 iOS 中的 狀態儲存和恢復功能,它能夠記住你的應用被系統強制殺死時的狀態,並在之後恢復此狀態。

備忘錄模式可以理解為在某個時刻捕捉、展示以及儲存任意例項的內部狀態,同時允許你可以在隨後的時間內查詢這些儲存下來的狀態並恢復它。當你恢復一個例項的某個狀態時,它應當完全反映出這個例項在被捕捉時的狀態。顯然,要達到此效果,你必須保證所有例項屬性的訪問許可權在捕捉和恢復時都是一樣的 —— 例如,public 的資料應恢復為 public 的屬性,private 的資料應恢復為 private 的屬性。

為了簡單起見,我使用 iOS 系統提供的 UserDefaults 作為我儲存和恢復例項狀態的核心工具。

備忘錄模式用例

在我知道了 iOS 本身已提供了便捷的歸檔和序列化的功能後,我隨即編寫了一些可以儲存和恢復類的狀態的示例程式碼。我的程式碼很出色地抽象出了歸檔和解檔的功能,因此你可以利用這些抽象方法儲存和恢復許多不同的例項和例項的屬性。不過我的示例程式碼並非用於生產環境下的,它們只是為了解釋備忘錄模式而編寫的教學性程式碼。

你可以在 Github 上找到我的 示例專案。這個專案展示了一個包含firstNamelastNameage 屬性的 User 類例項儲存在 UserDefaults 中,並隨後從 UserDefaults 恢復的過程。如同下面的效果一樣,一開始,並沒有任何 User 例項提供給我進行恢復,隨後我輸入了一個並把它歸檔,然後再恢復它:

MementoDemoApp

上面過程中控制檯輸出如下:

Empty entity.
 
lastName: Adams
age: 87
firstName: John
 
lastName: Adams
age: 87
firstName: John
複製程式碼

備忘錄模式示例程式碼

我所實現的備忘錄模式非常直白。程式碼中包含了一個 Memento 協議,以及 Memento 協議的擴充套件,用於在成員屬性中存在遵循 Memento 協議的屬性時,處理和抽象關於歸檔與解檔的邏輯。與此同時,這個協議擴充套件允許在任何時候列印例項的所有狀態。我使用了一個 Dictionary<String, String> 來儲存那些遵循協議的類中的屬性 —— 屬性名作為字典的 Key,屬性值作為字典的 Value。我把屬性的值以字串的型別儲存,以此達到程式碼較簡潔且容易理解的目的,但我必須承認實際情況中有許多用例會要求你去操作非常複雜的屬性型別。歸根到底,這是一個關於設計模式的教程,因此沒有任何程式碼是基於生產環境來編寫的。

需要注意我為 Memento 協議加了一個 persist() 方法和一個 recover() 方法,任何遵循此協議的類都必須實現它們。這兩個方法讓開發者可以根據實際需要,通過名字來歸檔和解檔某個遵循 Memento 協議的類中的特定屬性。換句話說,Memento 中型別為 Dictionary<String, String>state 屬性可以一對一地對應到某個遵循此協議的類中的屬性,這些屬性的名稱對應字典中元素的 key,屬性的值對應字典中元素的 value。相信你在看完具體的程式碼後肯定能完全理解。

由於遵循 Memento 協議的類必須實現 persist()recover() 方法,因此這兩個方法必須可以訪問所有可見的屬性,無論它具有什麼樣的訪問許可權 —— publicprivate 還是 fileprivate

你或許也想知道我為什麼把 Memento 協議設定為類協議(class-only)。原因僅僅是因為 Swift 編譯器那詭異的報錯:”Cannot use mutating member on immutable value: ‘self’ is immutable”。我們暫且不討論這個問題,因為它遠遠超出了本次教程的範圍。如果你對這個問題感興趣,你可以看一下這個 不錯的解釋

接下來就到了程式碼的部分。首先,你可以在我示例專案中的 Memento.swift 檔案找到關於備忘錄模式的核心實現:

import Foundation
 
// 由於"Cannot use mutating member on immutable value: ‘self’ is immutable"報錯問題,
// 此協議定義為類協議,僅適用於引用型別。
protocol Memento : class {
    
    // 訪問 UserDefaults 中 state 屬性的 key 值。
    var stateName: String { get }
    
    // 儲存遵循此協議的類當前狀態下的所有屬性名(key)和屬性值。
    var state: Dictionary<String, String> { get set }
    
    // 以特定的 stateName 為 key 將 state 屬性存入 UserDefaults 中。
    func save()
    
    // 以特定的 stateName 為 key 從 UserDefaults 中讀取 state。
    func restore()
    
    // 可自定義,以特定方式把遵循此協議的類的屬性儲存到 state 字典。
    func persist()
    
   // 可自定義,以特定方式從 state 字典讀取屬性。
    func recover()
    
    // 遍歷 state 字典並列印所有成員屬性,格式如下:
    // 
    // 屬性 1 名字(key):屬性 1 的值
    // 屬性 2 名字(key):屬性 2 的值
    func show()
    
} // Memento 協議結束
 
extension Memento {
    
    // 儲存 state 到磁碟中。
    func save() {
        UserDefaults.standard.set(state, forKey: stateName)
    }
    
    // 從磁碟中讀取 state。
    func restore() {
        
        if let dictionary = UserDefaults.standard.object(forKey: stateName) as! Dictionary<String, String>? {
            state = dictionary
        }
        else {
            state.removeAll()
        }
        
    } // restore() 函式結束
    
    // 以字典的形式儲存當前狀態可以很方便地進行視覺化輸出。
    func show() {
        
        var line = ""
        
        if state.count > 0 {
            
            for (key, value) in state {
                line += key + ": " + value + "
"
            }
            
            print(line)
            
        }
        
        else {
            print("Empty entity.
")
        }
            
    } // show() 函式結束
    
} // Memento 擴充套件結束
 
// 通過遵循 Memento 協議,任何類都可以方便地在整個應有執行期間
// 儲存其完整狀態,並能隨後任意時間進行讀取。
class User: Memento {
    
    // Memento 必須遵循的屬性。
    let stateName: String
    var state: Dictionary<String, String>
    
    // 此類獨有的幾個屬性,用於儲存系統使用者賬號。
    var firstName: String
    var lastName: String
    var age: String
    
    // 此構造器可用於儲存新使用者到磁碟,或者更新一個現有的使用者。
    // 持久化儲存所用的 key 值為 stateName 屬性。
    init(firstName: String, lastName: String, age: String, stateName: String) {
        
        self.firstName = firstName
        self.lastName = lastName
        self.age = age
        
        self.stateName = stateName
        self.state = Dictionary<String, String>()
        
        persist()
        
    } // 構造器定義結束
    
    // 此構造器可以從磁碟中讀取出一個已存在的使用者資訊。
    // 讀取所使用的 key 值為 stateName 屬性。
    init(stateName: String) {
        
        self.stateName = stateName
        self.state = Dictionary<String, String>()
        
        self.firstName = ""
        self.lastName = ""
        self.age = ""
        
        recover()
        
    } // 構造器定義結束
 
    // 持久化儲存使用者屬性。
    // 此處很直觀地將每個屬性一對一地以"屬性名-屬性值"的形式存入字典中。
    func persist() {
        
        state["firstName"] = firstName
        state["lastName"] = lastName
        state["age"] = age
        
        save() // leverage protocol extension
        
    } // persist() 函式結束
    
    // 讀取已儲存的使用者屬性。
    // 從 UserDefaults 中讀取了 state 字典後
    // 會簡單地以屬性名為 key 從字典中讀取出屬性值。
    func recover() {
        
        restore() // leverage protocol extension
            
        if state.count > 0 {
            firstName = state["firstName"]!
            lastName = state["lastName"]!
            age = state["age"]!
        }
        else {
            self.firstName = ""
            self.lastName = ""
            self.age = ""
        }
        
    } // recover() 函式結束
    
} // user 類結束
複製程式碼

接下來,你可以在示例專案中的 ViewController.swift 檔案中找到我上問所說的關於備忘錄模式的使用用例(對 User 類的歸檔和解檔):

import UIKit
 
class ViewController: UIViewController {
    
    @IBOutlet weak var firstNameTextField: UITextField!
    @IBOutlet weak var lastNameTextField: UITextField!
    @IBOutlet weak var ageTextField: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
 
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
 
    // "儲存使用者" 按鈕按下時呼叫此方法。
    // 以 "userKey" 作為 stateName 的值將 User 類例項的屬性
    // 儲存到 UserDefaults 中。
    @IBAction func saveUserTapped(_ sender: Any) {
        
        if firstNameTextField.text != "" &&
            lastNameTextField.text != "" &&
            ageTextField.text != "" {
            
            let user = User(firstName: firstNameTextField.text!,
                            lastName: lastNameTextField.text!,
                            age: ageTextField.text!,
                            stateName: "userKey")
            user.show()
            
        }
        
    } // saveUserTapped 函式結束
    
    // 在"恢復使用者"按鈕按下時呼叫此方法。
    // 以 "userKey" 作為 stateName 的值將 User 類例項的屬性
    // 從 UserDefaults 中讀取出來。
    @IBAction func restoreUserTapped(_ sender: Any) {
        
        let user = User(stateName: "userKey")
        firstNameTextField.text = user.firstName
        lastNameTextField.text = user.lastName
        ageTextField.text = user.age
        user.show()
 
    }
    
} // ViewController 類結束
複製程式碼

結論

即便 GoF 的設計模式已經在多數開發者心中被視為聖經般的存在(我在文章開頭提到的),但仍有某些對設計模式持有批評意見的人認為,設計模式的使用恰恰是我們對程式語言不夠了解或使用不夠巧妙的證明,而且在程式碼中頻繁使用設計模式並不是一件好事。我個人並不認同此看法。對於一些擁有幾乎所有能想象得到的特性的語言,例如 C++ 這種非常龐大的程式語言來說,這種意見或許會適用,但諸如此類的語言通常極其複雜以致於我們很難去學習、使用並掌握它。能夠識別出並解決一些重複出現的問題是我們作為人類的優點之一,我們並不應該抗拒它。而設計模式恰巧是人類從歷史錯誤中吸取教訓,並加以改進的絕佳例子。同時,設計模式對一些通用的問題給出了抽象化且標準化的解決方案,提高了解決這些問題的可能性和可部署性。

把一門簡潔的程式語言和一系列最佳實踐結合起來是一件美妙的事情,例如 Swift 和設計模式的結合。高一致性的程式碼通常也有高可讀性和高可維護性。同時你要記得一件事,設計模式是通過成千上萬的開發者們不斷地討論和交流想法而持續完善的。通過全球資訊網帶來的便捷性,開發者們在虛擬世界相互連線,他們的討論不斷碰撞出天才的火花。

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 swift.gg

相關文章