Swift Design Pattern 系列教程 #2:觀察者模式 (Observer) 與備忘錄模式 (Memento)


本教程是上週設計模式 (Design Patterns) 系列教程的第二部分。人稱「四人幫」(Gang of Four, GoF)的 Erich Gamma、Richard Helm、 Ralph Johnson 及 John Vlissides 所著的 “Design Patterns: Elements of Reusable Object-Oriented Software”,開創、收集、並解釋了目前常見的 23 種經典軟體開發設計模式。今天,我們將集中討論「行為 (Behavioral)」類別中的兩種模式:觀察者模式 (Observer)備忘錄模式 (Memento)

軟體開發是一種致力將現實世界情境模組化的過程,希望能夠建立工具來加強這個情境裡的使用者體驗。在財務管理方面的工具,例如銀行 App 或是購物工具如 Amazon 或 eBay 的 iOS App 等,絕對讓現在的生活比十年前來得方便。再想想我們至今走過的道路,雖然軟體 App 的功能越來越強大,對使用者亦更簡單易用;但對開發者來說,這種 App 的開發也變得更加複雜

因此,開發者建立了一系列管理複雜性的最佳實作方式,一些較熱門的例子像是物件導向程式設計 (Object-Oriented Programming)、協定導向程式設計 (Protocol-Oriented Programming)、數值語義 (Value Semantics)、Local Reasoning、拆分大型函式為多個有良好定義介面的小型函式(像是 Swift Extension)、語法糖 (Syntactic Sugar) 等。還有一個在眾多最佳實作最值得關注的,就是設計模式的使用。

設計模式

設計模式是一個非常重要的工具,讓開發者可以管理複雜的程式碼。要將其概念化的話,可以說它是一種樣版技術,而每個樣版都是量身訂做來解決相對應、重複出現、又容易識別的問題。你可以把它們用於構思程式情境的最佳實作清單,在構思的過程中你會反覆查看它們,像是如何從物件家族中建立物件、而不必了解物件家族的所有詳細實作細節。設計模式的重點便是它們適用於常見的場景上,因為它們是泛用的,所以可以重複使用。讓我舉一個具體的例子。

設計模式並不是特定於某些使用案例,像是迭代 (Iterating) 一個有 11 個整數 (Int) 的 Swift 陣列。舉例來說,四人幫定義迭代器 (Iterator) 模式來提供一個通用介面,以穿透集合裡的所有項目,而無需知道集合裡的複雜性(像是型別)。設計模式並不是編寫語言程式碼,而是一個解決常見軟體開發情境的一套準則或或經驗法則。

還記得我在 AppCoda 上談論過的 “Model-View-ViewModel” (MVVM) 設計模式,以及受到 Apple 與許多 iOS 開發者青睞的 “Model-View-Controller” (MVC) 設計模式嗎?

這兩個模式通常被應用在整個 App 上。MVVM 與 MVC 是架構 (Architectural) 設計模式,旨在將使用者界面 (UI) 與 App 的資料和呈現邏輯的程式碼分開,同時也將 App 的資料與核心資料處理及/或商業邏輯分開。四人幫的設計模式在性質上更為具體,旨在解決在 App 的程式庫更具體的問題。你可以在一個 App 裡使用三個、或七個、甚至十二個四人幫的設計模式。還記得我的迭代範例吧?雖然這不在四人幫的設計模式清單裡,但是代理 (Delegation) 是另一個很棒的設計模式

雖然四人幫的書對很多開發者來說就如聖經,但是亦有批評者存在的。我們將會在本篇文章的結論中討論這一點。

設計模式類別

四人幫將 23 個設計模式分為「創建 (Creational)」、「結構 (Structural)」及「行為 (Behavioral)」三大類別。本教程將討論行為類別中的兩種模式,而這個類別的目的,是對 class 和 struct(行為者)的行為加入安全、合乎常理、及一致的最佳實踐。我們希望整個應用程式內的行為者 (classes/structs) 都能獲得良好、一致、且可預測的行為;亦希望行為者在內部、以及與不同行為者之間有良好的互動。我們應該在編譯之前和編譯時考慮它們之間的行為互動關係;而在運行時,我們在 class/struct 裡面有很多實例 (instances),除了在做自己的事情外,它們不斷地與其它物件進行交互/溝通。由於實例之間的通信提升了軟體複雜性,因此制定一致、高效和安全的通信規則至關重要,但在構建每個 class/struct 時,這個概念不應減少對良好設計實踐的需求。由於我們專注於「行為」,因此在將責任分配給行為者以及它們的關連文件時,必須記住使用一致的模式。

在深入介紹理論前,先讓我提供實際例子,我將通過 Swift 程式碼實現的深入實踐來展示理論。在本教程中,我們將看到如何一致地分配維護行為者狀態的責任,以及如何一致地為行為者 (subject) 分配責任,向許多行為者的觀察者 (observer) 發送通知;另一方面,我們也會介紹如何一致地將責任分配給觀察者以便註冊獲得通知。

在討論設計模式時,一致性的概念應該是不言而喻的。請記住上回教程的重點、亦是我們介紹更多設計模式時將反覆提及的重點: 隱藏複雜度(封裝),這是聰明開發者的最終目標。例如,物件導向 (OOP) 類別能夠提供非常複雜而強大的功能,但不需要開發者了解關於類別內部的運作;同樣,Swift 協定導向 (POP) 亦是在控制複雜性方面,一種非常重要的技術。管理複雜性是開發人員最大的負擔,但現在我們就要談論如何馴服這頭野獸!

關於本教程定義的註釋

針對本教程,我將解釋濃縮於範例 App 程式碼註釋中。我將簡要介紹設計模式概念,希望讀者查看我的程式碼、閱讀當中的註解、並能夠完全內化我分享的技術。畢竟,如果你只能談論、而不能編寫程式碼,那麼你將無法通過面試,而且無法成為真正的核心開發人員。

你可能已經注意到我對行為設計模式的定義,是遵循 Apple 官方文件的

通常一個 Class 的實例稱為物件。然而在 Swift 中,Class 和 Structure 的關係要比在其他語言中更密切,本章中所討論的大部分功能都適用於 Class 和 Structure 上。因此,我們會使用更通用的術語實例,而不是物件。

我也在編譯時將 Class 和 Struct 稱為「行為者 (actors)」。

觀察者模式 (The observer design pattern)

你可能在使用 Apple 行動裝置時見過觀察者模式,亦可能在編寫 iOS App 時也注意到它。每當我住的地方下雨,不論 iPhone 是否為屏幕鎖定狀態,我們都會收到螢幕上彈出的推送通知,就如下圖所示:

PushNotification demo

訊息來源是由 Apple 代表國家氣象局向數千台 iPhone 發送廣播通知,警告大家所在地區有可能水浸。更具體來說,就像在 iOS App 中,一個實例 subject 將狀態轉變通知(許多)其他實例觀察者,而參與這個廣播類型通信的實例不需要互相知道彼此;這是鬆耦合 (loose coupling) 一個很好的例子。

Subject 實例 ── 通常是單個關鍵訊息源 ── 將關於其狀態更改的通知,廣播到依賴它的多個觀察者實例 (observer instances);其中,有興趣的觀察者必須訂閱以獲取通知。

值得慶幸的是,iOS 具有內置且眾所周知的功能,可用於啟用觀察者模式: NotificationCenter 。這點我就留給你們參考此處,自己研究一下。

觀察者設計模式的應用範例

我在 GitHub 上的觀察者範例專案,展示了這種廣播類型通信的運作。

我知道這並不符合 iOS 的 Human Interface Guidelines,但這裡需要一個實作範例,所以請不要太在意。假設我們採取主動方法,讓一個 Subject 實例負責監視網路連接。要實作這樣的 Broadcaster,你只需讓一個行為者採用我的 ObservedProtocol 即可。

假設有多個觀察者實例,例如一個 Image downloader(圖片下載)的 class、一個通過 REST API 驗證用戶憑證的 login 實例、以及一個訂閱 Subject 以獲得網絡連接狀態通知的 App 內瀏覽器,要實作這幾個實例,你可以使用任何類,並使其成為我採用 ObserverProtocol 的抽象 (abstract) Observer class 的繼承者。(我稍後會解釋將範例觀察者程式碼限制在一個 Class 中的原因。)

為實作範例專案中的觀察者設計模式,我創建了 NetworkConnectionHandler class。當這個 concrete class 的實例收到 NetworkConnectionStatus.connected 的通知時,它們將 UIView 實例變為綠色;當他們收到 NetworkConnectionStatus.disconnected 的通知時,則會將 UIView 實例變為紅色。

這是我在 iPhone 8 Plus 上編譯、安裝和運行範例程式碼的結果:

這是與以上動畫對應的 Xcode 控制台輸出:

觀察者設計模式 App 的範例程式碼

要查閱我在上一節中所指有大量註釋的程式碼,請查看範例專案中的 Observable.swift 文件:

我本來打算將大部分觀察者通知處理邏輯放入 ObserverProtocol 的 extension 中,但在設置 #selector 時遇到 @objc。所以,我想到使用 addObserver(forName:object:queue:using:) 的 block-based 版本,接著傳遞一個 notification handler closure ⋯⋯ 作為一個抽象類別,我決定讓我的通知處理程式碼更容易理解、更具有教育意義。

我也意識到 Swift 沒有 抽象類別的正式概念,但是你可能已經知道有一個常用的解決方法。因此,為了簡化解釋觀察者模式的目標,我透過強制 override 其 handleNotification() 方法,使 Observer class 「抽象化」。這樣一來,當你的 Observer 子類別實例收到通知時,你就有機會注入任何想執行的專屬邏輯。

下面是範例專案的 ViewController.swift 文件,展示了如何在 Observable.swift 中使用剛剛討論並審閱過的核心邏輯:

備忘錄 設計模式 (The memento design pattern)

大多數 iOS 開發人員都熟悉備忘錄模式,例如用於 Archives 和 Serialization 的 iOS 工具,它允許你「將物件和數值在屬性列表、JSON、及其他 Flat Binary 表示法之間相互轉換」。又例如 iOS 狀態保存和恢復功能,可以在「你的 App 被系統終止之後,記住並返回之前的狀態」。

備忘錄設計模式用於捕獲、表示、和儲存一個實例在特定時間點的內部狀態,然後允許你在稍後時間查找該實例的狀態、並恢復它。當你在恢復實例的狀態時,它應該準確反映被捕獲時的狀態。雖然這聽起來很簡單,但你應該確保在捕獲和恢復期間,所有實例屬性的訪問級別都有遵守到,例如:public 數據應該恢復到 public 屬性,而 private 數據應恢復到 private 屬性。

為讓事情簡單一點,我使用了 iOS 的 UserDefaults 作為實例狀態儲存和復原的核心。

備忘錄設計模式應用範例

雖然我知道 iOS 已經具備 archiving 和 serialization 功能,但我還是編寫了可以保存和恢復 Class 狀態的範例程式碼。我的程式碼可以很好地抽象化 archiving(封存)和 de-archiving(解壓縮),讓你可以儲存和恢復具有不同屬性的各種實例狀態。但我的例子不適合商業化產品使用,這只是一個用於解釋備忘錄模式的教學實例。

我在 GitHub 上的備忘錄範例專案,展示了一個包含了firstNamelastNameage 屬性的 User class 實例,其狀態如何可以永久保存到 UserDefaults 內,然後稍後再恢復。在開始時,我們沒有 User 實例可用於恢復,所以我輸入一組資料並將它儲存,然後就可以點擊 Restore User 恢復它,就像是這樣:

MementoDemoApp

這是與上一個動畫對應的控制台輸出:

備忘錄設計模式 App 的範例程式碼

我的備忘錄程式碼很簡單。它提供了 Memento 協定 (protocol) 和協定擴展 (protocol extension),用於處理並抽象化所有採用 Memento 協定的 Class 成員屬性其封存和解壓縮的所有細節。這個擴展還允許你在任何特定時間點,將物件的整個狀態打印到控制台。我使用了一個 Dictionary<String, String> 封存資料,將採用 Class 的屬性名稱作為儲存的 key、而屬性內容儲存為值,我亦將值存為 String 以保持我的實作簡單易懂,因為有許多其他範例會要求你封存和解壓縮更複雜的屬性類型,但這只是一個關於設計模式的教程,而不是商業化 App 的程式碼。

請注意,我將 persist()recover() 方法添加到 Memento 協定中,它們必須由任何採用 Memento 協定的 class 來實現。這些方法為開發者提供了機會,來按名稱封存和解壓縮採用 Memento 協定的特定 類別屬性。換句話說,Dictionary<String, String> 類型的 state 屬性元素,可以與採用 Memento 協定的類別屬性一對一匹配。每個屬性的名稱對應於 dictionary 元素的 key,而且每個屬性值對應於所述 key 匹配的 dictionary 元素的值。看看程式碼你就會明白了!

由於 persist()recover() 方法必須由採用 Memento 協定的 Class 實現,所以可以通過這些方法,查看和訪問所有訪問級別的屬性如 publicprivatefileprivate

你可能會問,為什麼我只使用 Memento 協定類別?我這樣做是因為那可怕的 Swift 編譯器訊息:”Cannot use mutating member on immutable value: ‘self’ is immutable”。這個討論已超出了本教程的範圍,但如果你想折磨自己,歡迎在此處閱讀相關描述。

這是我實作備忘錄設計模式的核心邏輯,你可以在範例應用程式的文件 Memento.swift 中找到:

這是實現之前提及的備忘錄設計模式範例的程式碼(封存和解壓 User 類別的實例),就如在範例應用程式中的 ViewController.swift 文件:

總結

一些評論說使用設計模式就證明了程式語言的不足之處,而且經常在程式碼看到模式出現不太好;我並不同意這看法。期待程式語言擁有所有功能是不切實際的,而且可能會導致本來已經很巨大的程式語言如 C++ 變得更巨大、更複雜,以致難以學習、使用和維護。了解並解決經常發生的問題是一個積極的特質,值得積極鼓勵。設計模式是人類從歷史中學到的成功範例。針對常見問題來寫出摘要並提出標準解決方法,讓這些解決方法可以被移植及分散出去。

像 Swift 這般的簡潔程式語言與最佳實踐範例(像是設計模式)結合,是一個理想而有趣的媒介。一致的程式碼通常都是可讀且可維護的。還要記住,隨著數百萬開發者不斷討論及分享想法,設計模式仍不斷發展。藉由網際網路連結在一起,這種開發者的討論就形成了不斷自我調整的集體智慧。

譯者簡介:陳奕先-過去為平面財經記者,專跑產業新聞,2015 年起跨進軟體開發世界,希望在不同領域中培養新的視野,於新創學校 ALPHA Camp 畢業後,積極投入 iOS 程式開發,目前任職於國內電商公司。聯絡方式:電郵 [email protected]

FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS

原文Design Patterns in Swift #2: Observer and Memento


熱愛寫作的多產作家,亦是軟體工程師、設計師、和開發員。最近專注於 Objective-C 和 Swift 的 iOS 手機 App 開發。但對於 C#、C++、.NET、JavaScript、HTML、CSS、jQuery、SQL Server、MySQL、Oracle、Agile、Test Driven Development、Git、Continuous Integration、Responsive Web Design 等。

blog comments powered by Disqus
訂閲電子報

訂閲電子報

AppCoda致力於發佈優質iOS程式教學,你不必每天上站,輸入你的電子郵件地址訂閱網站的最新教學文章。每當有新文章發佈,我們會使用電子郵件通知你。

已收你的指示。請你檢查你的電郵,我們已寄出一封認證信,點擊信中鏈結才算完成訂閱。

Shares
Share This