本文大部分内容翻译至《Pro Design Pattern In Swift》By Adam Freeman,一些地方做了些许修改,并将代码升级到了Swift2.0,翻译不当之处望多包涵。
单例模式
在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
理解单例模式解决的问题
单例模式保证了所给定的类型只有一个对象存在,并且所有的依赖这个对象的组件都使用同一个实例。这和原型模式不一样,原型模式是为了让拷贝对象更佳容易。相比之下,单例模式只允许一个对象的实例存在并阻止它被拷贝。
当你有一个对象并且你不想它在应用中被复制的时候,单例模式出现了。或者是因为它代表现实中的一种资源(例如打印机或者服务器),或者因为你想把一系列相关的活动合并到一起。请看下面例子:
BackupServer.swift
import Foundation
class DataItem {
enum ItemType : String {
case Email = "Email Address"
case Phone = "Telephone Number"
case Card = "Credit Card Number"
}
var type:ItemType
var data:String
init(type:ItemType, data:String) {
self.type = type
self.data = data
}
}
class BackupServer {
let name:String
private var data = [DataItem]()
init(name:String) {
self.name = name
}
func backup(item:DataItem) {
data.append(item)
}
func getData() -> [DataItem]{
return data
}
}
我们定义了一个BackuoServer类来代表一个服务器用来存储数据DataItem对象。接下来我们开始存储数据。
main.swift
var server = BackupServer(name:"Server#1")
server.backup(DataItem(type: DataItem.ItemType.Email, data: "[email protected]"))
server.backup(DataItem(type: DataItem.ItemType.Phone, data: "555-123-1133"))
var otherServer = BackupServer(name:"Server#2")
otherServer.backup(DataItem(type: DataItem.ItemType.Email, data: "[email protected]"))
这些代码可以编译也能执行,但是并没有什么实际意义。如果真正的目的是用BackupServer来代表一个现实中的备份服务器,那么任何人都能创造BackupServer实例并且调用backup方法的时候它又有何意义?
理解封装共享资源问题
单例模式不只是适用于代表现实资源的对象。有些场合是你想创建一个在应用中被所有组件能以简单又一致的方式调用的对象,请看下面的例子:
Logger.swift
import Foundation
class Logger {
private var data = [String]()
func log(msg:String) {
data.append(msg)
}
func printLog() {
for msg in data {
print("Log: \(msg)")
}
}
}
这是一个简单的日志类,可以在项目中用作简单的调试。Logger类定义了一个接受String类型参数并存储到数组中的log方法,printLog方法就打印出所有的信息。然后我们用它来对前面的BackupServer做日志记录。
main.swift
let logger = Logger()
var server = BackupServer(name:"Server#1")
server.backup(DataItem(type: DataItem.ItemType.Email, data: "[email protected]"))
server.backup(DataItem(type: DataItem.ItemType.Phone, data: "555-123-1133"))
logger.log("Backed up 2 items to \(server.name)")
var otherServer = BackupServer(name:"Server#2")
otherServer.backup(DataItem(type: DataItem.ItemType.Email, data: "[email protected]"))
logger.log("Backed up 1 item to \(otherServer.name)")
logger.printLog()
如果运行的话,会看见如下结果。
Log: Backed up 2 items to Server#1
Log: Backed up 1 item to Server#2
看似一切顺利,但是当我们想要对BackupServer类进行日志记录的时候,问题就出现了。
BackupServer.swift
...
class BackupServer {
let name:String
private var data = [DataItem]()
let logger = Logger()
init(name:String) {
self.name = name
logger.log("Created new server \(name)")
}
func backup(item:DataItem) {
data.append(item)
logger.log("\(name) backed up item of type \(item.type.rawValue)")
}
func getData() -> [DataItem]{
return data
}
}
...
我们不得不再创建一个Logger实例,所以现在就有两个Logger实例了。而且我们在main.swift中调用printLog方法的话也不会输出BackupServer中的日志信息。我们想要的是一个Logger对象就能记录并输出应用中所有组件的日志信息-这就是所谓的封装一个共享资源。
理解单例模式
单例模式可以通过确保只有一个对象来解决代表现实世界资源的对象问题和共享资源封装问题。这个对象也叫单例被所有的组件分享,如下图:
实现单例模式
当实现单例模式的时候,必须遵守一些规则:
- 单例必须只有一个实例存在
- 单例不能被其他对象代替,即使是相同的类型
- 单例必须能被使用它的组件定位到
Note:单例模式只能对引用类型起作用,这意味着只有类才支持单例。当被赋值给变量的时候,结构体和其他类型都会被拷贝所以不起作用。拷贝引用类型的唯一方法是通过它的初始化方法或者依赖NSCopying协议。
快速实现单例模式
实现单例模式最快的方法是声明一个静态变量持有自己的一个实例,并将初始化方法私有化。再看我们的Logger类:
Logger.swift
import Foundation
class Logger {
private var data = [String]()
static let sharedInstance = Logger()
private init(){}
func log(msg:String) {
data.append(msg)
}
func printLog() {
for msg in data {
print("Log: \(msg)")
}
}
}
接着我们修改BackupServer.swift和main.swift中关于日志记录的代码:
BackupServer.swift
...
static let server = (name:"MainServer")
private init(name:String) {
self.name = name
Logger.sharedInstance.log("Created new server \(name)")
}
func backup(item:DataItem) {
data.append(item)
Logger.sharedInstance.log("\(name) backed up item of type \(item.type.rawValue)")
}
...
main.swift
import Foundation
var server = BackupServer.server
server.backup(DataItem(type: DataItem.ItemType.Email, data: "[email protected]"))
server.backup(DataItem(type: DataItem.ItemType.Phone, data: "555-123-1133"))
Logger.sharedInstance.log("Backed up 2 items to \(server.name)")
var otherServer = BackupServer.server
otherServer.backup(DataItem(type: DataItem.ItemType.Email, data: "[email protected]"))
Logger.sharedInstance.log("Backed up 1 item to \(otherServer.name)")
Logger.sharedInstance.printLog()
现在如果运行代码,将看见如下输出:
Log: Created new server MainServer
Log: MainServer backed up item of type Email Address
Log: MainServer backed up item of type Telephone Number
Log: Backed up 2 items to MainServer
Log: MainServer backed up item of type Email Address
Log: Backed up 1 item to MainServer
处理并发
如果你在一个多线程的应用里使用单例,那么你就得考虑不同的组件同时并发的操作单例并且防止一些潜在的问题。并发问题很常见,因为Swift数组不是线程安全的,所以我们的Logger和BackupServer类在并发访问时都会出问题。这就是意味着可能会有两个以上的线程会在同一时间调用数组的append方法从而破坏数据结构。为了说明这个问题,我们可以对main.swift做一些修改。
main.swift
import Foundation
var server = BackupServer.server
let queue = dispatch_queue_create("workQueue", DISPATCH_QUEUE_CONCURRENT)
let group = dispatch_group_create()
for count in 0..<100 {
dispatch_group_async(group, queue, { () -> Void in
BackupServer.server.backup(DataItem(type: DataItem.ItemType.Email,
data: "[email protected]"))
})
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
print("\(server.getData().count) items were backed up")
这里用GCD异步的去调用BackServer单例的backup方法100次。GCD用的C的API,所以语法不像Swift。我们这样创建了一个队列:
...
let queue = dispatch_queue_create("workQueue", DISPATCH_QUEUE_CONCURRENT)
...
dispatch_queue_create方法接受两个参数用来分别设置队列的名称和类型。 我们这里设置队列的名称叫workQueue,使用常量DISPATCH_QUEUE_CONCURRENT来指定队列中的块应该被多线程并发的执行。我们将生成的队列对象赋值给了一个常量queue,这个queue的常量类型是dispatch_queue_t。
为了使当所有的块都被执行后能够接收到一个通知,我们将它们放进一个组,用dispatch_group_create来创建组。
...
let group = dispatch_group_create()
...
为了异步到提交所有任务,我们用dispatch_group_async方法来向队列中提交执行的块。
...
dispatch_group_async(group, queue, {() in
BackupServer.server.backup(DataItem(type: DataItem.ItemType.Email, data: "[email protected]"))
})
...
第一个参数是与块相关的组,第二个参数是块被添加到的队列,最后一个参数就是块自己了,用一个闭包来表示。这个闭包没有参数也没有返回值。GCD会将每一个任务块从队列中取出并异步执行--虽然,你也知道,队列也可用于执行串行任务。
最后一步就是我们要等到100个任务全部执行完,像这么做:
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
dispatch_group_wait会中断当前的线程直到组中的所有块都被执行结束为止。第一个参数是被监视的组,第二个参数是等待时间。使用DISPATCH_TIME_FOREVER的话,就意味着会一直无限制等下去直到所有任务块执行完成。
为了看清这个问题,现在我们只需要简单的执行程序即可:
序列化访问
为了解决这个问题,我们必须确保在某个时间点对于数组只能有一个执行块去调用append方法。下面将展示我们如何用GCD来解决这个问题。
BackupServer.swift
import Foundation
class DataItem {
enum ItemType : String {
case Email = "Email Address"
case Phone = "Telephone Number"
case Card = "Credit Card Number"
}
var type:ItemType
var data:String
init(type:ItemType, data:String) {
self.type = type
self.data = data
}
}
class BackupServer {
let name:String;
private var data = [DataItem]()
static let server = BackupServer(name: "MainServer")
private let queue = dispatch_queue_create("arrayQ", DISPATCH_QUEUE_SERIAL)
private init(name:String) {
self.name = name
Logger.sharedInstance.log("Created new server \(name)")
}
func backup(item:DataItem) {
dispatch_sync(queue) { () -> Void in
self.data.append(item)
Logger.sharedInstance.log("\(self.name) backed up item of type \(item.type.rawValue)")
}
}
func getData() -> [DataItem]{
return data;
}
}
这次我们做的正好和上面main.swift中相反。我们接受一些列的异步块并且强迫它们串行的执行以此来保证在任何一个时间点只有一个块去调用数组的append方法。
...
private let arrayQ = dispatch_queue_create("arrayQ", DISPATCH_QUEUE_SERIAL)
...
第一个参数是队列名称,第二个参数DISPATCH_QUEUE_SERIAL指定了队列中的块会被取出并且一个接一个的执行。任何一个块都不会开始执行直到它前一个块执行完毕。
在backup方法中我们用dispatch_sync方法将块添加到队列中。
...
func backup(item:DataItem) {
dispatch_sync(queue) { () -> Void in
self.data.append(item)
Logger.sharedInstance.log("\(self.name) backed up item of type \(item.type.rawValue)")
}
}
...
dispatch_sync 方法将任务添加到队列中的方式就像前面 dispatch_group_async方法一样,但是它会等待知道块执行完成才返回,dispatch_group_async方法却是立即返回。
Logger类也存在同样的并发问题,现在我们也用同样的方法修改它。
Logger.swift
import Foundation
class Logger {
private var data = [String]()
static let sharedInstance = Logger()
private let queue = dispatch_queue_create("arrayQ", DISPATCH_QUEUE_SERIAL)
private init(){}
func log(msg:String) {
dispatch_sync(queue) { () -> Void in
self.data.append(msg)
}
}
func printLog() {
for msg in data {
print("Log: \(msg)")
}
}
}
如果现在执行程序,将不会出现数据错误问题,后台会输出以下内容:
100 items were backed up
理解单例模式的陷阱
-
泄露陷阱
最常见的问题就是实现单例时创建了一个可以被复制的对象。可能因为是误用了结构体(或者其它内建的相关类型),也可能是误用了实现NSCopying的类。
-
并发陷阱
最棘手的问题就是跟单例模式相关的并发了,对于很有经验的程序员来说也是很大的话题。
-
忘记考虑并发
第一个问题就是当需要并发保护的时候却没有做。并不是所有的单例都会面临并发问题,但是并发是一个你必须要严肃考虑在内的问题。如果你依赖分享数据,比如数组,或者全局方法,例如print,那么你必须保证你的相关代码不能被多线程并发访问。
-
始终坚持并发保护
单例模式中必须坚持并发保护,这样所有操作一个资源(例如数组)的代码才能以同一种方式串行化。如果你让仅仅一个方法或者块不是串行化的访问数组,那么你就将面对两个冲突的线程和数据错误。