讓觀察者模式變得更美好
OSX 已經(jīng)有至少 17 年的歷史垮庐,而NotificationCenter在其第一次版本發(fā)布就已經(jīng)存在,并且一直是蘋果開發(fā)者常用的工具。對于不了解的人來說,NotificationCenter 是基于觀察者模式的概念卷胯,也是軟件設(shè)計模式中行為型模式的一部分。
觀察者模式
觀察者模式由Gang of Four在 90 年代中期提出并一直存在威酒,是一種比較容易理解的設(shè)計模式窑睁。首先,會存在一個被稱之為觀察目標(biāo)的對象葵孤;這個對象維護一個包含觀察者的列表担钮,并將狀態(tài)的變化通知給這些觀察者。
舉個真實的例子佛呻。你所在的城市有一家繁忙的咖啡店裳朋。不少顧客在排隊買咖啡,咖啡師會詢問顧客的姓名吓著,并將其寫在杯子上鲤嫡,以便分清楚咖啡是誰點的;然后讓顧客禮貌地等待其名字被叫绑莺。每制作完一杯咖啡暖眼,咖啡師會叫出杯子上所寫的名字,從而讓顧客愉快地取到自己所點的咖啡纺裁。
在這種情況下诫肠,咖啡師是觀察目標(biāo)司澎,購買咖啡的顧客是觀察者,而咖啡是狀態(tài)的變化栋豫,因為咖啡從一個空杯變成了滿滿一杯含咖啡因的美味挤安。
NotificationCenter的問題
對于寫代碼的我們,觀察者模式毫無疑問是一種有很多用途的偉大模式丧鸯。但同時不得不承認蛤铜,我從來不是它的狂熱粉絲,并非因為缺乏一些好的理由:
保證觀察對象的一致性
如果一個項目中沒有強制性的標(biāo)準丛肢,那么實現(xiàn)和向觀察者發(fā)送通知的方式可能就會多種多樣围肥。例如混亂的通知名稱:
classBarista{
letnotification ="coffeeMadeNotification"
}
classTrainee{
letcoffeeMadeNotificationName ="Coffee Made"
}
避免通知名稱沖突
如果開發(fā)者隨意給通知起名,那么兩個不同的觀察對象則可能擁有相同的通知名蜂怎,于是無論這兩者誰發(fā)出一個采用此名字的通知穆刻,錯誤的觀察者便可能會收到此通知。
假設(shè)咖啡店里有兩個咖啡師杠步,如果每個咖啡師都用相同的通知名氢伟,顧客便會收到毫無意義的通知,甚至更糟的是篮愉,會收到一杯含有大豆印度茶并且不含咖啡因的香草拿鐵而不是一杯拿鐵咖啡腐芍。
classBarista{
staticletcoffeeMadeNotification ="coffeeMadeNotification"
}
classTrainee:Barista{ }
...
NotificationCenter.default.
.postNotificationName(Trainee.coffeeMadeNotification)
使用字符串作為名稱的通知
我會避免使用字符串類型的通知差导,你也應(yīng)該如此试躏,因為這樣只會產(chǎn)出容易出錯的代碼。永遠不要相信人們避免拼寫錯誤或在沒有自動補全功能環(huán)境下編程的能力设褐。
NSNotificationCenter.defaultCenter()
.postNotificationName("coffeeMadNotfication")
替代方案
更多的時候颠蕴,我會盡可能使用代理模式來代替觀察者模式。代理模式與觀察者模式非常相似助析,但并不是一對多的關(guān)系犀被,代理模式是一對一的關(guān)系。雖然代理模式也有自己的一些問題和限制外冀,但它避免了我上面列出的問題寡键,所以在我看來這種模式是更可靠的選擇。不過今天并不會深入探討這些問題雪隧。
通知協(xié)議
protocolNotifier{ }
我們可以設(shè)計一個協(xié)議來解決上面列出的所有問題西轩,于是接下來挨個研究下這些問題,然后實現(xiàn)一個更 Swift 化的脑沿、有統(tǒng)一變化的NSNotificationCenter實現(xiàn)藕畔。
保證觀察對象的一致性
協(xié)議非常有用,因為想要遵守某個協(xié)議庄拇,就必須強制符合其規(guī)范注服。所以針對于這個協(xié)議韭邓,我們將給它設(shè)置一個關(guān)聯(lián)類型:
protocolNotifier{
associatedTypeNotification:RawRepresentable
}
從現(xiàn)在開始,如果在項目中的類或結(jié)構(gòu)體想要發(fā)布通知溶弟,那就應(yīng)該遵守Notifier協(xié)議女淑,并提供遵守RawRepresentable協(xié)議的關(guān)聯(lián)類型。
classBarista:Notifier{
enumNotification:String{
casemakingCoffee
casecoffeeMade
}
}
在 Swift 中辜御,由于枚舉也可以遵守RawRepresentable協(xié)議诗力,所以可以使用一個String類型的枚舉,并命名相應(yīng)的通知我抠。
letcoffeeMade =Barista.Notification.coffeeMade.rawValue
NSNotificationCenter.defaultCenter()
.postNotificationName(coffeeMade)
避免通知名稱沖突
同樣苇本,枚舉在這方面也起了很大作用,因為它可以讓我們避免重復(fù)定義菜拓。如果我們創(chuàng)建了多個makeCoffee的枚舉瓣窄,編譯器將提示錯誤。然而纳鼎,這并不能解決具有不同類或結(jié)構(gòu)但具有相同枚舉名稱的問題。
letbaristaNotification =Barista.Notification.coffeeMade.rawValue
lettraineeNotification =Trainee.Notification.coffeeMade.rawValue
// baristaNotification: coffeeMade
// traineeNotification: coffeeMade
如上所見贱鄙,需要為這些通知創(chuàng)建一個唯一的命名空間劝贸,來保證通知名稱之間沒有任何沖突逗宁。使用對應(yīng)的對象名稱是一種很好的解決方案,因為編譯器不允許類或結(jié)構(gòu)體具有相同的名稱瞎颗。
letbaristaNotification =
"\(Barista).\(Barista.Notification.coffeeMade.rawValue)"
lettraineeNotification =
"\(Trainee).\(Trainee.Notification.coffeeMade.rawValue)"
// baristaNotification: Barista.coffeeMade
// traineeNotification: Trainee.coffeeMade
到目前為止都很順利,但是現(xiàn)在我們的實現(xiàn)方案到了一個左右為難的境地哼拔。一方面引有,我們解決了命名空間重復(fù)的問題,但另一方面我們的代碼看起來像是一坨垃圾倦逐。的確譬正,雖然已經(jīng)實現(xiàn)了一些統(tǒng)一性,但是如果沒有任何保護措施來防止我們自己和協(xié)作的開發(fā)人員忘記添加命名空間檬姥,那么這個方案是毫無意義的吧曾我?
通知實現(xiàn)
對你來說幸運的是,我自己已經(jīng)考慮到這一點穿铆,并避免了上述的糟糕情況您单。我們將進一步擴展我們的協(xié)議,并在 NSNotificationCenter 功能調(diào)用方面添加一些很友好的符合Swift API 指南的荞雏、特定類型的語法糖虐秦。
通知名稱
Barista.coffeeMade
我們通常希望使用自己的通知命名空間和名稱平酿,因此會創(chuàng)建一個以通知枚舉為參數(shù)的函數(shù),這個函數(shù)會在我們發(fā)出通知和移除觀察者時返回安全的通知名稱悦陋。這個函數(shù)也是私有的蜈彼,因為我們并不希望外部的代碼訪問此功能,而是由自己和同事強制地遵守通知協(xié)議俺驶,從而具備了本來實現(xiàn)不了的優(yōu)點幸逆。