創(chuàng)建者模式-單例模式(The Singleton Pattern)

本文大部分內(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ù)錯誤。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末尚镰,一起剝皮案震驚了整個濱河市膛虫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌钓猬,老刑警劉巖稍刀,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異敞曹,居然都是意外死亡账月,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門澳迫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來局齿,“玉大人,你說我怎么就攤上這事橄登∽ゼ撸” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵拢锹,是天一觀的道長谣妻。 經(jīng)常有香客問我,道長卒稳,這世上最難降的妖魔是什么蹋半? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮充坑,結果婚禮上减江,老公的妹妹穿的比我還像新娘。我一直安慰自己捻爷,他們只是感情好辈灼,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著也榄,像睡著了一般巡莹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上手蝎,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天榕莺,我揣著相機與錄音,去河邊找鬼棵介。 笑死钉鸯,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的邮辽。 我是一名探鬼主播唠雕,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼贸营,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了岩睁?” 一聲冷哼從身側響起钞脂,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎捕儒,沒想到半個月后冰啃,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡刘莹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年阎毅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片点弯。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡扇调,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出抢肛,到底是詐尸還是另有隱情狼钮,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布捡絮,位于F島的核電站熬芜,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏锦援。R本人自食惡果不足惜猛蔽,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望灵寺。 院中可真熱鬧,春花似錦区岗、人聲如沸略板。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽叮称。三九已至,卻和暖如春藐鹤,著一層夾襖步出監(jiān)牢的瞬間瓤檐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工娱节, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留挠蛉,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓肄满,卻偏偏與公主長得像谴古,于是被迫代替她去往敵國和親质涛。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容

  • GCD簡介 GCD 是 libdispatch 的市場名稱掰担,而 libdispatch 作為 Apple 的一個庫...
    獨木舟的木閱讀 1,235評論 0 5
  • 盡管 Grand Central Dispatch (GCD)已經(jīng)存在一段時間了汇陆,但并非每個人都知道怎么使用它。這...
    coderFamer閱讀 7,362評論 1 16
  • 在這篇文章中带饱,我將為你整理一下 iOS 開發(fā)中幾種多線程方案毡代,以及其使用方法和注意事項。當然也會給出幾種多線程的案...
    張戰(zhàn)威ican閱讀 601評論 0 0
  • 41.多用派發(fā)隊列,少用同步鎖 在Objective-C中勺疼,如果有多個線程要執(zhí)行同一份代碼教寂,那么有時可能會出問題。...
    Code_Ninja閱讀 1,127評論 1 13
  • 學習多線程恢口,轉(zhuǎn)載兩篇大神的帖子孝宗,留著以后回顧!第一篇:關于iOS多線程耕肩,你看我就夠了 第二篇:GCD使用經(jīng)驗與技巧...
    John_LS閱讀 606評論 0 3