本文大部分内容翻译至《Pro Design Pattern In Swift》By Adam Freeman,一些地方做了些许修改,并将代码升级到了Swift2.0,翻译不当之处望多包涵。
对象池模式(The Object Pool Pattern)
对象池模式是单例模式的一个变种,它提供了获取一系列相同对象实例的入口。当你需要对象来代表一组可替代资源的时候就变的很有用,每个对象每次可以被一个组件使用。
理解对象池模式解决的问题
在许多项目中,有时候对象的实例数目可能会有限制。请看下面例子:
Book.swift
import Foundation
class Book {
let author:String
let title:String
let stockNumber:Int
var reader:String?
var checkoutCount = 0
init(author:String, title:String, stock:Int) {
self.author = author
self.title = title
self.stockNumber = stock
}
}
在一个追踪图书馆书的系统中,创建或者克隆Book对象都不适用于现实中的图书馆中的书。同样的如果使用单例模式也不行,因为图书馆里可不止一本书。
图书管中的每一本书都有可能在某个时候被读者借出并且以后不能再被人使用直到归还。当书在库的时候读者可以立即借出,但是当库存耗尽的时候,任何想要再借这本书的人都必须等到某个人还书或者图书馆增加库存。
理解对象池模式
对象池模式管理一个可代替对象的集合。组件从池中借出对象,用它来完成一些任务并当任务完成时归还该对象。被归还的对象接着满足请求,不管是同一个组件还是其他组件的请求。对象池模式可以管理那些代表的现实资源或者通过重用来分摊昂贵初始化代价的对象。
第二步操作就是借出。
第三步操作是组件用借出的对象来完成一些任务。这时候并不需要对象池再做什么,但是这也意味着该对象将被租借一段时间并且不能在被其他组件借出。
第四步操作就是归还,组件归还借出的对象这样可以继续满足其他的租借请求。
在一个多线程的应用中,第二,第三,第四步操作都有可能发生并发操作。多线程的组件中分享对象导致了潜在的并发问题。
也存在一种情况就是当所有对象都被借出时不能满足接下来的请求,对象池必须应对这些请求,不管是告诉组件已经没有对象可借还是允许组件等待直到有归还的对象。
实现对象池模式
-
定义对象池类
首先是定义一个对象池的泛型类,这倒不一定非得是泛型,只是说利用泛型可以更好的重用代码。
Pool.swift
class Pool {
private var data = [T]()
init(items:[T]) {
data.reserveCapacity(items.count)
for item in items {
data.append(item)
}
}
func getFromPool() -> T? {
var result:T?
if (data.count > 0) {
result = self.data.removeAtIndex(0)
}
return result
}
func returnToPool(item:T) {
self.data.append(item)
}
}
这个Pool类,更准确的说是Pool
-
保护对象池数组
处理并发问题对于对象池来说十分重要,这里有两个问题我们需要解决。遇到的第一个问题和单例模式遇到的一样,getFromPool方法和returnToPool方法都操作了数组,我们需要确保当两个线程同时调用时不会出问题。
Pool.swift
import Foundation
class Pool {
private var data = [T]()
private let queue = dispatch_queue_create("arrayQ", DISPATCH_QUEUE_SERIAL)
init(items:[T]) {
data.reserveCapacity(items.count)
for item in items {
data.append(item)
}
}
func getFromPool() -> T? {
var result:T?
if (data.count > 0) {
dispatch_sync(queue, { () -> Void in
result = self.data.removeAtIndex(0)
})
}
return result
}
func returnToPool(item:T) {
dispatch_async(queue) { () -> Void in
self.data.append(item)
}
}
}
-
确保对象能借出
在Pool类中其实还有第二个并发问题。在getFromPool方法中,我们检查了在对象池中的对象是否为0。
Pool.swift
...
if (data.count > 0) {
dispatch_sync(queue, { () -> Void in
result = self.data.removeAtIndex(0)
})
}
...
这是一个经典的并发问题。想象一下对象池中只剩下了一个对象,但此时两个线程在极短的时间间隔下去调用getFromPool 方法。第一个线程检查了data.count发现不为0后调用 dispatch_sync并获取对象。
极短时间内,第二个线程做了相同的事情。相信还有一个对象存在,但是当它试图获取这个对象的时候,线程池却是空的。
我们同样用GCD来解决这个问题:
Pool.swift
import Foundation
class Pool {
private var data = [T]()
private let queue = dispatch_queue_create("arrayQ", DISPATCH_QUEUE_SERIAL)
private let semaphore:dispatch_semaphore_t
init(items:[T]) {
data.reserveCapacity(items.count)
for item in items {
data.append(item)
}
semaphore = dispatch_semaphore_create(items.count)
}
func getFromPool() -> T? {
var result:T?
if (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) == 0) {
dispatch_sync(queue, { () -> Void in
result = self.data.removeAtIndex(0)
})
}
return result
}
func returnToPool(item:T) {
dispatch_async(queue) { () -> Void in
self.data.append(item)
dispatch_semaphore_signal(self.semaphore)
}
}
}
消费对象池
现在我们已经创建好了一个泛型的对象池。我们可以创建图书馆了。
Library.swift
import Foundation
class Library {
private var books:[Book]
private let pool:Pool
static let sharedInstance = Library(stockLevel: 2)
private init(stockLevel:Int) {
books = [Book]()
for count in 1 ... stockLevel {
books.append(Book(author: "Dickens, Charles", title: "Hard Times",
stock: count))
}
pool = Pool(items:books)
}
func checkoutBook(reader:String) -> Book? {
let book = pool.getFromPool()
book?.reader = reader
book?.checkoutCount++
return book
}
func returnBook(book:Book) {
book.reader = nil
pool.returnToPool(book)
}
func printReport() {
for book in books {
print("...Book#\(book.stockNumber)...")
print("Checked out \(book.checkoutCount) times")
if (book.reader != nil) {
print("Checked out to \(book.reader!)")
} else {
print("In stock")
}
}
}
}
Library类通过结合Pool类实现了对象池模式。同时Library我们做成了单例,因为这里我们只有一个图书馆。注意到这里我们将Library类和Book类定义在了不同的文件,但我们却没有保护Book类使得它可以在Library类以外实例化。
接着我们模拟并发借书还书过程:
main.swift
import Foundation
var queue = dispatch_queue_create("workQ", DISPATCH_QUEUE_CONCURRENT)
var group = dispatch_group_create()
print("Starting...")
for i in 1 ... 20 {
dispatch_group_async(group, queue, {() in
var book = Library.sharedInstance.checkoutBook("reader#\(i)")
if (book != nil) {
NSThread.sleepForTimeInterval(Double(rand() % 2))
Library.sharedInstance.returnBook(book!)
}
})
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
print("All blocks complete")
Library.sharedInstance.printReport()
如果我们执行代码,可能得到下面结果(因为 NSThread.sleepForTimeInterval的缘故,结果可能会有所不同,但是借书总次数是一样的20次):
Starting...
All blocks complete
...Book#1...
Checked out 7 times
In stock
...Book#2...
Checked out 13 times
In stock
Cocoa中的对象池模式
Cocoa在公开的API里并没有暴露对象池,除了一个例外:table cell对象。
...
let cell = tableView.dequeueReusableCellWithIdentifier("ProductCell")
as ProductTableCell
...
这是一个请求获取一个ProductTableCell对象的方法。dequeueReusableCellWithIdentifier方法结合了对象池模式和工厂模式。UIKit框架负责管理UITableViewCell的创建和分配,使得它们可以重用。