让观察者模式变得更美好
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
我们通常希望使用自己的通知命名空间和名称,因此会创建一个以通知枚举为参数的函数,这个函数会在我们发出通知和移除观察者时返回安全的通知名称。这个函数也是私有的,因为我们并不希望外部的代码访问此功能,而是由自己和同事强制地遵守通知协议,从而具备了本来实现不了的优点。
网友评论