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