本文大部分内容翻译至《Pro Design Pattern In Swift》By Adam Freeman,一些地方做了些许修改,并将代码升级到了Swift2.0,翻译不当之处望多包涵。
代理模式(The Proxy Pattern)
为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
示例工程
OS X Command Line Tool 工程:
main.swift
import Foundation
func getHeader(header:String) {
let url = NSURL(string: "http://www.apress.com")
let request = NSURLRequest(URL: url!)
NSURLSession.sharedSession().dataTaskWithRequest(request,
completionHandler: {data, response, error in
if let httpResponse = response as? NSHTTPURLResponse {
if let headerValue = httpResponse.allHeaderFields[header] as? NSString {
print("\(header): \(headerValue)")
}
}
}).resume()
}
let headers = ["Content-Length", "Content-Encoding"]
for header in headers {
getHeader(header)
}
NSFileHandle.fileHandleWithStandardInput().availableData
main.swift中对Apress的主页进行HTTP请求并打印Content-Length 和 Content-Encoding 的值,运行程序可以看到下面输出:
Content-Encoding: gzip
Content-Length: 27003
你也可能看见Content-Length先输出。这是因为Foundation框架的HTTP请求是异步( asynchronous)和并发的(concurrent),这意味着任何一个请求都可能先完成然后第一个输出结果。
理解代理模式解决的问题
代理模式主要用来解决三个不同的问题。
理解远程对象问题
远程对象问题就是当你需要处理网络资源时,例如一个网页或者网络服务。main.swift中的代码去获取一个网页,但是它并没有将提供的机制分离出来。在代码中没有抽象和封装,这导致了如果我们修改实现的话会影响到其它的代码。
理解昂贵花销问题
像HTTP请求那样的需要昂贵开销的任务。例如需要大量计算,大量内存消耗,需要消耗设备电量(GPS),带宽要求,运行时间等。
对于HTTP请求来说,主要的代价是运行时间和带宽,和服务器生成并返回response。main.swift中并没有对HTTP请求做任何优化,也没有考虑过用户,网络,或者服务器接受请求等带来的冲突。
理解访问权限问题
限制访问一个对象通常存在于多用户的应用中。你不能改变想保护对象的定义因为你没有代码权限或者对象有其它的依赖。
理解代理模式
代理模式可以用在当一个对象需要用来代表其他的资源。如下面的图,资源可以是一些抽象,例如一个网页,或者其他对象。
解决远程对象问题
用代理模式来代表远程对象或是资源是从分布式系统开始的,例如CORBA。CORBA提供了一个暴露了和服务器对象相同方法的本地对象。本地对象是代理,并且调用了一个和远程对象相关联的方法。CORBA十分注意代理对象和远程对象的映射并处理参数和结果。
CORBA并没有流行起来,但是随着HTTP成为了传输选择和RESTful服务的流行,代理模式变得很重要。代理模式可以让操作远程资源变得十分容易。
代理对象隐藏了如何访问远程资源的细节,仅仅提供了它的数据。
解决昂贵开销问题
代理可以通过将运算和使用去耦来减小开销。在示例工程中,我们可以将不同的HTTP头值请求放在一次请求中。
解决访问权限问题
代理可以被当作一个对象的外包装,增加一个额外的逻辑来强制执行一些约束。
实现代理模式
实现代理模式根据解决的问题不同而不同。
实现解决远程对象问题
实现用代理模式去访问一个远程对象的关键是要分离出请求对象需要远程对象提供的特征。
1.定义协议
Proxy.swift
protocol HttpHeaderRequest {
func getHeader(url:String, header:String) -> String?
}
2.定义代理实现类
Proxy.swift
import Foundation
protocol HttpHeaderRequest {
func getHeader(url:String, header:String) -> String?
}
class HttpHeaderRequestProxy : HttpHeaderRequest {
private let semaphore = dispatch_semaphore_create(0)
func getHeader(url: String, header: String) -> String? {
var headerValue:String?
let nsUrl = NSURL(string: url)
let request = NSURLRequest(URL: nsUrl!)
NSURLSession.sharedSession().dataTaskWithRequest(request,
completionHandler: {data, response, error in
if let httpResponse = response as? NSHTTPURLResponse {
headerValue = httpResponse.allHeaderFields[header] as? String
}
dispatch_semaphore_signal(self.semaphore)
}).resume()
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER)
return headerValue
}
}
HttpHeaderRequestProxy类实现了HttpHeaderRequest 协议。getHeader方法用了Foundation框架去发动HTTP请求,同时增加了GCD信号量用来覆盖异步请求的问题。
Tip:在真正的项目中我并不建议在同步的方法中去覆盖异步操作,并不仅仅是因为Swift闭包使得操作异步任务很容易。
3.使用远程对象代理
main.swift
import Foundation
let url = "http://www.apress.com"
let headers = ["Content-Length", "Content-Encoding"]
let proxy = HttpHeaderRequestProxy()
for header in headers {
if let val = proxy.getHeader(url, header:header) {
print("\(header): \(val)")
}
}
NSFileHandle.fileHandleWithStandardInput().availableData
实现解决昂贵花销问题
有很多种方法可以优化昂贵花销的操作,比较常用的如缓存,延迟加载,或者如享元模式的其他设计模式。
示例工程中昂贵的操作就是HTTP请求,减小HTTP请求次数会得到一个本质的改善效果。显然示例中的最佳优化方法是对于想获得多个不同的HTTP头文件时只发起一次HTTP请求。
Proxy.swift
import Foundation
protocol HttpHeaderRequest {
func getHeader(url:String, header:String) -> String?
}
class HttpHeaderRequestProxy : HttpHeaderRequest {
private let queue = dispatch_queue_create("httpQ", DISPATCH_QUEUE_SERIAL)
private let semaphore = dispatch_semaphore_create(0)
private var cachedHeaders = [String:String]()
func getHeader(url: String, header: String) -> String? {
var headerValue:String?
dispatch_sync(self.queue){[weak self] in
if let cachedValue = self!.cachedHeaders[header] {
headerValue = "\(cachedValue) (cached)"
}else {
let nsUrl = NSURL(string: url)
let request = NSURLRequest(URL: nsUrl!)
NSURLSession.sharedSession().dataTaskWithRequest(request,
completionHandler: {data, response, error in
if let httpResponse = response as? NSHTTPURLResponse {
let headers = httpResponse.allHeaderFields as! [String: String]
for (name, value) in headers {
self!.cachedHeaders[name] = value
}
headerValue = httpResponse.allHeaderFields[header] as? String
}
dispatch_semaphore_signal(self!.semaphore)
}).resume()
dispatch_semaphore_wait(self!.semaphore, DISPATCH_TIME_FOREVER)
}
}
return headerValue
}
}
getHeader方法创建了一个缓存字典用来满足随后的请求,运行程序我们可以得到下面的结果:
Content-Length: 26992
Content-Encoding: gzip (cached)
注意:HttpHeaderRequestProxy 类增加了一个GCD同步队列来确保一次请求就更新字典的缓存数据。
延迟操作
另一个常用的方法就是尽可能长的延迟执行昂贵花销的操作。这样做的好处是昂贵的花销可能被取消,坏处是耦合性强。
Proxy.swift
import Foundation
protocol HttpHeaderRequest {
init(url:String)
func getHeader(header:String, callback:(String, String?) -> Void )
func execute()
}
class HttpHeaderRequestProxy : HttpHeaderRequest {
let url:String
var headersRequired:[String: (String, String?) -> Void]
required init(url: String) {
self.url = url
self.headersRequired = Dictionary Void>()
}
func getHeader(header: String, callback: (String, String?) -> Void) {
self.headersRequired[header] = callback;
}
func execute() {
let nsUrl = NSURL(string: url)
let request = NSURLRequest(URL: nsUrl!)
NSURLSession.sharedSession().dataTaskWithRequest(request,
completionHandler: {data, response, error in
if let httpResponse = response as? NSHTTPURLResponse {
let headers = httpResponse.allHeaderFields as! [String: String]
for (header, callback) in self.headersRequired {
callback(header, headers[header])
}
}
}).resume()
}
}
如果用户在调用execute方法之前因某种原因放弃了,那么HTTP请求就不会执行。
main.swift
import Foundation
let url = "http://www.apress.com"
let headers = ["Content-Length", "Content-Encoding"]
let proxy = HttpHeaderRequestProxy(url: url)
for header in headers {
proxy.getHeader(header, callback: {header, val in
if (val != nil) {
print("\(header): \(val!)")
}
});
}
proxy.execute()
NSFileHandle.fileHandleWithStandardInput().availableData
执行程序,得到下面结果:
Content-Encoding: gzip
Content-Length: 26992
实现解决访问权限问题
创建授权资源
Auth.swift
class UserAuthentication {
var user:String?
var authenticated:Bool = false
static let sharedInstance = UserAuthentication()
private init() {
// do nothing - stops instances being created
}
func authenticate(user:String, pass:String) {
if (pass == "secret") {
self.user = user
self.authenticated = true
} else {
self.user = nil
self.authenticated = false
}
}
}
UserAuthentication用单例模式来提供用户认证,事实上用户的密码是secret的时候认证成功。在真实的项目中,认证是很复杂的,可能会有自己的代理。
创建代理对象
接下来就是创建代理对象:
Proxy.swift
import Foundation
protocol HttpHeaderRequest {
init(url:String)
func getHeader(header:String, callback:(String, String?) -> Void )
func execute()
}
class AccessControlProxy : HttpHeaderRequest {
private let wrappedObject: HttpHeaderRequest
required init(url:String) {
wrappedObject = HttpHeaderRequestProxy(url: url)
}
func getHeader(header: String, callback: (String, String?) -> Void) {
wrappedObject.getHeader(header, callback: callback)
}
func execute() {
if (UserAuthentication.sharedInstance.authenticated) {
wrappedObject.execute()
} else {
fatalError("Unauthorized")
}
}
}
private class HttpHeaderRequestProxy : HttpHeaderRequest {
let url:String
var headersRequired:[String: (String, String?) -> Void]
required init(url: String) {
self.url = url
self.headersRequired = Dictionary Void>()
}
func getHeader(header: String, callback: (String, String?) -> Void) {
self.headersRequired[header] = callback;
}
func execute() {
let nsUrl = NSURL(string: url)
let request = NSURLRequest(URL: nsUrl!)
NSURLSession.sharedSession().dataTaskWithRequest(request,
completionHandler: {data, response, error in
if let httpResponse = response as? NSHTTPURLResponse {
let headers = httpResponse.allHeaderFields as! [String: String]
for (header, callback) in self.headersRequired {
callback(header, headers[header])
}
}
}).resume()
}
}
注意到 HttpHeaderRequestProxy 类前面的private,因为对于代理类来说子类也没有任何意义。
应用代理
main.swift
import Foundation
let url = "http://www.apress.com"
let headers = ["Content-Length", "Content-Encoding"]
let proxy = AccessControlProxy(url: url)
for header in headers {
proxy.getHeader(header){header, val in
if (val != nil) {
print("\(header): \(val!)")
}
}
}
UserAuthentication.sharedInstance.authenticate("bob", pass: "secret")
proxy.execute()
NSFileHandle.fileHandleWithStandardInput().availableData
运行程序:
Content-Encoding: gzip
Content-Length: 26992
代理模式的变形
代理类可以用来引用计数( reference counting),当资源要求主动式管理或者当你需要引用数到一定的时候做一些分类的时候变得很有用。为了说明这个问题,我们创建了另一个示例项目ReferenceCounting:
NetworkRequest.swift
import Foundation
protocol NetworkConnection {
func connect()
func disconnect()
func sendCommand(command:String)
}
class NetworkConnectionFactory {
class func createNetworkConnection() -> NetworkConnection {
return NetworkConnectionImplementation()
}
}
private class NetworkConnectionImplementation : NetworkConnection {
typealias me = NetworkConnectionImplementation
static let sharedQueue:dispatch_queue_t = dispatch_queue_create("writeQ",DISPATCH_QUEUE_SERIAL)
func connect() { me.writeMessage("Connect") }
func disconnect() { me.writeMessage("Disconnect") }
func sendCommand(command:String) {
me.writeMessage("Command: \(command)")
NSThread.sleepForTimeInterval(1)
me.writeMessage("Command completed: \(command)")
}
private class func writeMessage(msg:String) {
dispatch_async(self.sharedQueue){ _ in
print(msg)
}
}
}
NetworkConnection协议定义的方法用来操作一个假象的服务器。connect用来建立和服务器的连接,sendCommand方法用来操作服务器,当任务完成调用disconnect 方法断开连接。
NetworkConnectionImplementation类实现了这个协议并且输出调试日志。用单例模式创建了一个GCD串行队列,这样两个对象同时去输出日志就不会出现问题。
同时用工厂模式来提供对NetworkConnectionImplementation类的实例的入口,因为我们可以看见NetworkConnectionImplementation前面的private关键字使得其他文件无法访问。
main.swift
import Foundation
let queue = dispatch_queue_create("requestQ", DISPATCH_QUEUE_CONCURRENT)
for count in 0 ..< 3 {
let connection = NetworkConnectionFactory.createNetworkConnection()
dispatch_async(queue){ _ in
connection.connect()
connection.sendCommand("Command: \(count)")
connection.disconnect()
}
}
NSFileHandle.fileHandleWithStandardInput().availableData
运行程序:
Connect
Connect
Connect
Command: Command: 0
Command: Command: 1
Command: Command: 2
Command completed: Command: 1
Disconnect
Command completed: Command: 2
Disconnect
Command completed: Command: 0
Disconnect
实现引用计数代理
在这种情况中,引用计数代理可以用来网络连接的生命周期,所以可以服务不同的请求。
NetworkRequest.swift
import Foundation
protocol NetworkConnection {
func connect()
func disconnect()
func sendCommand(command:String)
}
class NetworkConnectionFactory {
static let sharedWrapper = NetworkRequestProxy()
private init(){
// do nothing
}
}
private class NetworkConnectionImplementation : NetworkConnection {
typealias me = NetworkConnectionImplementation
static let sharedQueue:dispatch_queue_t = dispatch_queue_create("writeQ",DISPATCH_QUEUE_SERIAL)
func connect() { me.writeMessage("Connect") }
func disconnect() { me.writeMessage("Disconnect") }
func sendCommand(command:String) {
me.writeMessage("Command: \(command)")
NSThread.sleepForTimeInterval(1)
me.writeMessage("Command completed: \(command)")
}
private class func writeMessage(msg:String) {
dispatch_async(self.sharedQueue){ _ in
print(msg)
}
}
}
class NetworkRequestProxy : NetworkConnection {
private let wrappedRequest:NetworkConnection
private let queue = dispatch_queue_create("commandQ", DISPATCH_QUEUE_SERIAL)
private var referenceCount:Int = 0
private var connected = false
init() {
wrappedRequest = NetworkConnectionImplementation()
}
func connect() { /* do nothing */ }
func disconnect() { /* do nothing */ }
func sendCommand(command: String) {
self.referenceCount++
dispatch_sync(self.queue){[weak self]in
if (!self!.connected && self!.referenceCount > 0) {
self!.wrappedRequest.connect()
self!.connected = true
}
self!.wrappedRequest.sendCommand(command)
self!.referenceCount--
if (self!.connected && self!.referenceCount == 0) {
self!.wrappedRequest.disconnect()
self!.connected = false
}
}
}
}
我修改了工厂模式类,现在用单例模式返回一个NetworkRequestProxy 类的实例。 NetworkRequestProxy实现了NetworkRequest协议同时它是NetworkConnectionImplementation对象的一个外层类。
这里的引用计数代理的目的是为了将 connect和disconnect方法剥离请求并且保持连接直到等待的命令操作完成为止。我们用串行队列来保证每个时间点只有一个命令在运行,并且保证了引用计数不会受并发访问的影响。
main.swift
import Foundation
let queue = dispatch_queue_create("requestQ", DISPATCH_QUEUE_CONCURRENT)
for count in 0 ..< 3 {
let connection = NetworkConnectionFactory.sharedWrapper
dispatch_async(queue){ _ in
connection.connect()
connection.sendCommand("Command: \(count)")
connection.disconnect()
}
}
NSFileHandle.fileHandleWithStandardInput().availableData
运行程序:
Connect
Command: Command: 0
Command completed: Command: 0
Command: Command: 1
Command completed: Command: 1
Command: Command: 2
Command completed: Command: 2
Disconnect
从输出结果我们可以看出,command现在在一次连接中串行的执行。代理减少了并发访问的次数,但是同时串行化了每一个操作,这意味着会运行时间变长了。