创建者模式-单例模式(The Singleton Pattern)

本文大部分内容翻译至《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,那么你必须保证你的相关代码不能被多线程并发访问。

  • 始终坚持并发保护

单例模式中必须坚持并发保护,这样所有操作一个资源(例如数组)的代码才能以同一种方式串行化。如果你让仅仅一个方法或者块不是串行化的访问数组,那么你就将面对两个冲突的线程和数据错误。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容