本文大部分內(nèi)容翻譯至《Pro Design Pattern In Swift》By Adam Freeman豺旬,一些地方做了些許修改,并將代碼升級到了Swift2.0,翻譯不當之處望多包涵柒凉。
單例模式
在應用這個模式時族阅,單例對象的類必須保證只有一個實例存在。許多時候整個系統(tǒng)只需要擁有一個的全局對象膝捞,這樣有利于我們協(xié)調(diào)系統(tǒng)整體的行為坦刀。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數(shù)據(jù)由一個單例對象統(tǒng)一讀取鲤遥,然后服務進程中的其他對象再通過這個單例對象獲取這些配置信息沐寺。這種方式簡化了在復雜環(huán)境下的配置管理。
理解單例模式解決的問題
單例模式保證了所給定的類型只有一個對象存在盖奈,并且所有的依賴這個對象的組件都使用同一個實例混坞。這和原型模式不一樣,原型模式是為了讓拷貝對象更佳容易卜朗。相比之下拔第,單例模式只允許一個對象的實例存在并阻止它被拷貝咕村。
當你有一個對象并且你不想它在應用中被復制的時候场钉,單例模式出現(xiàn)了⌒柑危或者是因為它代表現(xiàn)實中的一種資源(例如打印機或者服務器)逛万,或者因為你想把一系列相關的活動合并到一起。請看下面例子:
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類來代表一個服務器用來存儲數(shù)據(jù)DataItem對象批钠。接下來我們開始存儲數(shù)據(jù)宇植。
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"))
這些代碼可以編譯也能執(zhí)行,但是并沒有什么實際意義埋心。如果真正的目的是用BackupServer來代表一個現(xiàn)實中的備份服務器指郁,那么任何人都能創(chuàng)造BackupServer實例并且調(diào)用backup方法的時候它又有何意義?
理解封裝共享資源問題
單例模式不只是適用于代表現(xiàn)實資源的對象拷呆。有些場合是你想創(chuàng)建一個在應用中被所有組件能以簡單又一致的方式調(diào)用的對象闲坎,請看下面的例子:
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)")
}
}
}
這是一個簡單的日志類,可以在項目中用作簡單的調(diào)試茬斧。Logger類定義了一個接受String類型參數(shù)并存儲到數(shù)組中的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類進行日志記錄的時候怖喻,問題就出現(xiàn)了。
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
}
}
...
我們不得不再創(chuàng)建一個Logger實例岁诉,所以現(xiàn)在就有兩個Logger實例了锚沸。而且我們在main.swift中調(diào)用printLog方法的話也不會輸出BackupServer中的日志信息。我們想要的是一個Logger對象就能記錄并輸出應用中所有組件的日志信息-這就是所謂的封裝一個共享資源唉侄。
理解單例模式
單例模式可以通過確保只有一個對象來解決代表現(xiàn)實世界資源的對象問題和共享資源封裝問題咒吐。這個對象也叫單例被所有的組件分享,如下圖:
實現(xiàn)單例模式
當實現(xiàn)單例模式的時候,必須遵守一些規(guī)則:
- 單例必須只有一個實例存在
- 單例不能被其他對象代替恬叹,即使是相同的類型
- 單例必須能被使用它的組件定位到
Note:單例模式只能對引用類型起作用候生,這意味著只有類才支持單例。當被賦值給變量的時候绽昼,結構體和其他類型都會被拷貝所以不起作用唯鸭。拷貝引用類型的唯一方法是通過它的初始化方法或者依賴NSCopying協(xié)議硅确。
快速實現(xiàn)單例模式
實現(xiàn)單例模式最快的方法是聲明一個靜態(tài)變量持有自己的一個實例目溉,并將初始化方法私有化。再看我們的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()
現(xiàn)在如果運行代碼菱农,將看見如下輸出:
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
處理并發(fā)
如果你在一個多線程的應用里使用單例缭付,那么你就得考慮不同的組件同時并發(fā)的操作單例并且防止一些潛在的問題。并發(fā)問題很常見循未,因為Swift數(shù)組不是線程安全的割疾,所以我們的Logger和BackupServer類在并發(fā)訪問時都會出問題汽纠。這就是意味著可能會有兩個以上的線程會在同一時間調(diào)用數(shù)組的append方法從而破壞數(shù)據(jù)結構。為了說明這個問題,我們可以對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異步的去調(diào)用BackServer單例的backup方法100次琴拧。GCD用的C的API踩衩,所以語法不像Swift责静。我們這樣創(chuàng)建了一個隊列:
...
let queue = dispatch_queue_create("workQueue", DISPATCH_QUEUE_CONCURRENT)
...
dispatch_queue_create方法接受兩個參數(shù)用來分別設置隊列的名稱和類型。 我們這里設置隊列的名稱叫workQueue星虹,使用常量DISPATCH_QUEUE_CONCURRENT來指定隊列中的塊應該被多線程并發(fā)的執(zhí)行零抬。我們將生成的隊列對象賦值給了一個常量queue,這個queue的常量類型是dispatch_queue_t搁凸。
為了使當所有的塊都被執(zhí)行后能夠接收到一個通知媚值,我們將它們放進一個組,用dispatch_group_create來創(chuàng)建組护糖。
...
let group = dispatch_group_create()
...
為了異步到提交所有任務褥芒,我們用dispatch_group_async方法來向隊列中提交執(zhí)行的塊。
...
dispatch_group_async(group, queue, {() in
BackupServer.server.backup(DataItem(type: DataItem.ItemType.Email, data: "bob@example.com"))
})
...
第一個參數(shù)是與塊相關的組嫡良,第二個參數(shù)是塊被添加到的隊列锰扶,最后一個參數(shù)就是塊自己了,用一個閉包來表示寝受。這個閉包沒有參數(shù)也沒有返回值坷牛。GCD會將每一個任務塊從隊列中取出并異步執(zhí)行--雖然,你也知道很澄,隊列也可用于執(zhí)行串行任務京闰。
最后一步就是我們要等到100個任務全部執(zhí)行完颜及,像這么做:
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
dispatch_group_wait會中斷當前的線程直到組中的所有塊都被執(zhí)行結束為止。第一個參數(shù)是被監(jiān)視的組蹂楣,第二個參數(shù)是等待時間俏站。使用DISPATCH_TIME_FOREVER的話,就意味著會一直無限制等下去直到所有任務塊執(zhí)行完成痊土。
為了看清這個問題肄扎,現(xiàn)在我們只需要簡單的執(zhí)行程序即可:
序列化訪問
為了解決這個問題,我們必須確保在某個時間點對于數(shù)組只能有一個執(zhí)行塊去調(diào)用append方法赁酝。下面將展示我們?nèi)绾斡肎CD來解決這個問題犯祠。
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中相反。我們接受一些列的異步塊并且強迫它們串行的執(zhí)行以此來保證在任何一個時間點只有一個塊去調(diào)用數(shù)組的append方法酌呆。
...
private let arrayQ = dispatch_queue_create("arrayQ", DISPATCH_QUEUE_SERIAL)
...
第一個參數(shù)是隊列名稱衡载,第二個參數(shù)DISPATCH_QUEUE_SERIAL指定了隊列中的塊會被取出并且一個接一個的執(zhí)行。任何一個塊都不會開始執(zhí)行直到它前一個塊執(zhí)行完畢肪笋。
在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方法一樣,但是它會等待知道塊執(zhí)行完成才返回藤乙,dispatch_group_async方法卻是立即返回。
Logger類也存在同樣的并發(fā)問題惭墓,現(xiàn)在我們也用同樣的方法修改它坛梁。
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)")
}
}
}
如果現(xiàn)在執(zhí)行程序,將不會出現(xiàn)數(shù)據(jù)錯誤問題腊凶,后臺會輸出以下內(nèi)容:
100 items were backed up
理解單例模式的陷阱
-
泄露陷阱
最常見的問題就是實現(xiàn)單例時創(chuàng)建了一個可以被復制的對象划咐。可能因為是誤用了結構體(或者其它內(nèi)建的相關類型)钧萍,也可能是誤用了實現(xiàn)NSCopying的類褐缠。
-
并發(fā)陷阱
最棘手的問題就是跟單例模式相關的并發(fā)了,對于很有經(jīng)驗的程序員來說也是很大的話題风瘦。
-
忘記考慮并發(fā)
第一個問題就是當需要并發(fā)保護的時候卻沒有做队魏。并不是所有的單例都會面臨并發(fā)問題,但是并發(fā)是一個你必須要嚴肅考慮在內(nèi)的問題万搔。如果你依賴分享數(shù)據(jù)胡桨,比如數(shù)組,或者全局方法瞬雹,例如print昧谊,那么你必須保證你的相關代碼不能被多線程并發(fā)訪問。
-
始終堅持并發(fā)保護
單例模式中必須堅持并發(fā)保護酗捌,這樣所有操作一個資源(例如數(shù)組)的代碼才能以同一種方式串行化呢诬。如果你讓僅僅一個方法或者塊不是串行化的訪問數(shù)組涌哲,那么你就將面對兩個沖突的線程和數(shù)據(jù)錯誤。