Swift: NotificationCenter 協議
讓觀察者模式變得更美好
OSX 已經有至少 17 年的歷史,而 NotificationCenter 在其第一次版本發布就已經存在,并且一直是蘋果開發者常用的工具。對于不了解的人來說,NotificationCenter 是基于 觀察者模式 的概念,也是軟件設計模式中行為型模式的一部分。
觀察者模式
觀察者模式由 Gang of Four 在 90 年代中期提出并一直存在,是一種比較容易理解的設計模式。首先,會存在一個被稱之為觀察目標的對象;這個對象維護一個包含觀察者的列表,并將狀態的變化通知給這些觀察者。
舉個真實的例子。你所在的城市有一家繁忙的咖啡店。不少顧客在排隊買咖啡,咖啡師會詢問顧客的姓名,并將其寫在杯子上,以便分清楚咖啡是誰點的;然后讓顧客禮貌地等待其名字被叫。每制作完一杯咖啡,咖啡師會叫出杯子上所寫的名字,從而讓顧客愉快地取到自己所點的咖啡。
在這種情況下,咖啡師是觀察目標,購買咖啡的顧客是觀察者,而咖啡是狀態的變化,因為咖啡從一個空杯變成了滿滿一杯含咖啡因的美味。
NotificationCenter的問題
對于寫代碼的我們,觀察者模式毫無疑問是一種有很多用途的偉大模式。但同時不得不承認,我從來不是它的狂熱粉絲,并非因為缺乏一些好的理由:
保證觀察對象的一致性
如果一個項目中沒有強制性的標準,那么實現和向觀察者發送通知的方式可能就會多種多樣。例如混亂的通知名稱:
class Barista {
let notification = "coffeeMadeNotification"
}
class Trainee {
let coffeeMadeNotificationName = "Coffee Made"
}
避免通知名稱沖突
如果開發者隨意給通知起名,那么兩個不同的觀察對象則可能擁有相同的通知名,于是無論這兩者誰發出一個采用此名字的通知,錯誤的觀察者便可能會收到此通知。
假設咖啡店里有兩個咖啡師,如果每個咖啡師都用相同的通知名,顧客便會收到毫無意義的通知,甚至更糟的是,會收到一杯含有大豆印度茶并且不含咖啡因的香草拿鐵而不是一杯拿鐵咖啡。
class Barista {
static let coffeeMadeNotification = "coffeeMadeNotification"
}
class Trainee : Barista { }
...
NotificationCenter.default.
.postNotificationName(Trainee.coffeeMadeNotification)
使用字符串作為名稱的通知
我會避免使用字符串類型的通知,你也應該如此,因為這樣只會產出容易出錯的代碼。永遠不要相信人們避免拼寫錯誤或在沒有自動補全功能環境下編程的能力。
NSNotificationCenter.defaultCenter()
.postNotificationName("coffeeMadNotfication")
替代方案
更多的時候,我會盡可能使用代理模式來代替觀察者模式。代理模式與觀察者模式非常相似,但并不是一對多的關系,代理模式是一對一的關系。雖然代理模式也有自己的一些問題和限制,但它避免了我上面列出的問題,所以在我看來這種模式是更可靠的選擇。不過今天并不會深入探討這些問題。
通知協議
protocol Notifier { }
我們可以設計一個協議來解決上面列出的所有問題,于是接下來挨個研究下這些問題,然后實現一個更 Swift 化的、有統一變化的 NSNotificationCenter 實現。
保證觀察對象的一致性
協議非常有用,因為想要遵守某個協議,就必須強制符合其規范。所以針對于這個協議,我們將給它設置一個 關聯類型 :
protocol Notifier {
associatedType Notification: RawRepresentable
}
從現在開始,如果在項目中的類或結構體想要發布通知,那就應該遵守 Notifier 協議,并提供遵守 RawRepresentable 協議的關聯類型。
class Barista : Notifier {
enum Notification : String {
case makingCoffee
case coffeeMade
}
}
在 Swift 中,由于枚舉也可以遵守 RawRepresentable 協議,所以可以使用一個 String 類型的枚舉,并命名相應的通知。
let coffeeMade = Barista.Notification.coffeeMade.rawValue
NSNotificationCenter.defaultCenter()
.postNotificationName(coffeeMade)
避免通知名稱沖突
同樣,枚舉在這方面也起了很大作用,因為它可以讓我們避免重復定義。如果我們創建了多個 makeCoffee 的枚舉,編譯器將提示錯誤。然而,這并不能解決具有不同類或結構但具有相同枚舉名稱的問題。
let baristaNotification = Barista.Notification.coffeeMade.rawValue
let traineeNotification = Trainee.Notification.coffeeMade.rawValue
// baristaNotification: coffeeMade
// traineeNotification: coffeeMade
如上所見,需要為這些通知創建一個唯一的命名空間,來保證通知名稱之間沒有任何沖突。使用對應的對象名稱是一種很好的解決方案,因為編譯器不允許類或結構體具有相同的名稱。
let baristaNotification =
"\(Barista).\(Barista.Notification.coffeeMade.rawValue)"
let traineeNotification =
"\(Trainee).\(Trainee.Notification.coffeeMade.rawValue)"
// baristaNotification: Barista.coffeeMade
// traineeNotification: Trainee.coffeeMade
到目前為止都很順利,但是現在我們的實現方案到了一個左右為難的境地。一方面,我們解決了命名空間重復的問題,但另一方面我們的代碼看起來像是一坨垃圾。的確,雖然已經實現了一些統一性,但是如果沒有任何保護措施來防止我們自己和協作的開發人員忘記添加命名空間,那么這個方案是毫無意義的吧?
通知實現
對你來說幸運的是,我自己已經考慮到這一點,并避免了上述的糟糕情況。我們將進一步擴展我們的協議,并在 NSNotificationCenter 功能調用方面添加一些很友好的符合 Swift API 指南 的、特定類型的語法糖。
通知名稱
Barista.coffeeMade
我們通常希望使用自己的通知命名空間和名稱,因此會創建一個以 通知 枚舉為參數的函數,這個函數會在我們發出通知和移除觀察者時返回安全的通知名稱。這個函數也是 私有 的,因為我們并不希望外部的代碼訪問此功能,而是由自己和同事強制地遵守 通知 協議,從而具備了本來實現不了的優點。
添加觀察者
Barista.addObserver(customer, selector: .coffeeMadeNotification, notification: .coffeeMade)
從現在開始,如果我們給一個觀察對象添加觀察者,就必須直接告知這個類。通過這樣的方式,我們的代碼閱讀和編寫的時候就顯得更易懂,因為能夠明確知道觀察者正在監聽這個觀察對象的通知。
注意:如果覺得 .coffeeMadeNotfication 選擇器參數很比較陌生,我建議閱讀下我之前的一篇文章: 選擇器語法糖 。
發送通知
Barista.postNotification(.coffeeMade)
這很蠢吧?可不是嘛!不過現在發通知就好多了。通過避免使用 NSNotificationCenter.defaultCenter() 的冗長的方式調用,同時為 object 和 userInfo 設置了 nil 默認值,因此調用發送通知的方法變得相當的簡介。我們也能夠確認,當前通知不會與其他類發生沖突,因為通知的名稱是由遵守協議對象類的名字拼接而成的。
移除通知
Barista.removeObserver(customer, notification: .coffeeMade)
跟 addObserver 的 API 一樣,只需要告知這個類把某個 Notification 的觀察者從其觀察者列表中移除即可。
其他
通知協議還具有更多的功能,它能利用可變參數的特性,通過一行代碼和實例函數來注銷多個通知,但考慮到這篇帖子的本意,我并沒有實現這個功能,因為這并不符合我們最初的需求。本文中沒有列出的代碼都在文章的底部的示例代碼中。
示例代碼
目前為止,我們已經將 NSNotificationCenter 封裝到 Notifier 擴展中,并且解決了項目協作中可能出現的忘記附加命名空間的問題,同時讓代碼看起來更優雅。不相信么?那就親自來查看一下:
通過觀察對象對觀察者列表的管理,我們已經消除了所有常見的與 NSNotificationCenter 使用相關的歧義。所以從現在開始,如果觀察者想要注冊或者停止接收通知,那么就必須通知觀察對象并修改其觀察者列表。
跟之前一樣,為了防止暫時無法使用 Xcode 的情況,我在 GitHub 上提供了一個 playgrounds ,您可以下載下來,同時還有一個 Gist 。
來自:http://swift.gg/2017/04/13/swift-nsnotificationcenter-protocol/