本文大部分內容翻譯至《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: "joe@example.com"))
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: "bob@example.com"))
這些代碼可以編譯也能執行,但是并沒有什么實際意義。如果真正的目的是用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: "joe@example.com"))
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: "bob@example.com"))
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: "joe@example.com"))
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: "bob@example.com"))
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: "bob@example.com"))
})
}
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: "bob@example.com"))
})
...
第一個參數是與塊相關的組,第二個參數是塊被添加到的隊列,最后一個參數就是塊自己了,用一個閉包來表示。這個閉包沒有參數也沒有返回值。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,那么你必須保證你的相關代碼不能被多線程并發訪問。
-
始終堅持并發保護
單例模式中必須堅持并發保護,這樣所有操作一個資源(例如數組)的代碼才能以同一種方式串行化。如果你讓僅僅一個方法或者塊不是串行化的訪問數組,那么你就將面對兩個沖突的線程和數據錯誤。