结构模式-代理模式(The Proxy Pattern)

本文大部分内容翻译至《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<String, (String, String?) -> 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<String, (String, String?) -> 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现在在一次连接中串行的执行。代理减少了并发访问的次数,但是同时串行化了每一个操作,这意味着会运行时间变长了。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,591评论 18 139
  • 1 场景问题# 1.1 访问多条数据## 考虑这样一个实际应用:要一次性访问多条数据。 这个功能的背景是这样的;在...
    七寸知架构阅读 2,968评论 1 52
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,857评论 6 13
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,121评论 29 470
  • If the bell rings Please bury me in feathers When night f...
    梅照阅读 170评论 0 0