1. 什么是代理模式
代理模式(Proxy Pattern) 定义
Provide a surrogate or placeholder for another object to control access to it.(为其他对象提供一种代理以控对这个对象的访问)
是给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
2. 角色组成
代理模式的主要角色如下:
- 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
UML图如下:
3. 代码示例
以海外代购为例。假设想要购买海外商品,一种方式呢,当然是我们自己到国外商店(OverSeasStore)进行购买,这个虽然可行,可是耗费额外时间和交通成本太高了。这个时候一批有商业头脑的人做起了代购(OverSeasAgent),有购买什么产品的需要直接找他们就行了,他们通过他们的渠道帮忙到海外商店进行购买你要的产品,然后发货给你。他们从中赚取了佣金,而我们则节约了成本。
- 首先定义商店售货的接口(抽象主题)
// StoreInterface.h
#import <Foundation/Foundation.h>
@protocol StoreInterface <NSObject>
- (void)sale:(NSString *)product;
@end
- 定义海外商店OverSeasStore(真实主题类)
// OverSeasStore.h
#import <Foundation/Foundation.h>
#import "StoreInterface.h"
@interface OverSeasStore : NSObject<StoreInterface>
@end
// OverSeasStore.m
#import "OverSeasStore.h"
@implementation OverSeasStore
- (void)sale:(NSString *)product {
NSLog(@"在海外商店购买了:%@", product);
}
@end
- 定义代购类OverSeasAgent(代理),购买过程代购需要从海外商店购买用户想要的商品
// OverSeasAgent.h
#import <Foundation/Foundation.h>
#import "StoreInterface.h"
@interface OverSeasAgent : NSObject<StoreInterface>
@end
// OverSeasAgent.m
#import "OverSeasAgent.h"
#import "OverSeasStore.h"
@interface OverSeasAgent ()
@property (nonatomic, strong) OverSeasStore *store;
@end
@implementation OverSeasAgent
- (void)sale:(NSString *)product {
NSLog(@"客户在代购商店购买了:%@", product);
[self.store sale:product];
[self packAndSend];
}
- (void)packAndSend {
NSLog(@"代购对商品进行打包");
NSLog(@"代购把商品发送给客户");
}
- (OverSeasStore *)store {
if (!_store) {
_store = [OverSeasStore new];
}
return _store;
}
@end
- 调用测试,购买找代购购买奶粉
+ (void)test {
OverSeasAgent *agent = [OverSeasAgent new];
[agent sale:@"奶粉"];
}
运行结果
4. 分析
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理对象可以扩展目标对象的功能;
- 具体主题角色是随时都会发生变化的,甭管它如何变化,只要它实现了接口,那我们的代理类完全就可以在不做任何修改的情况下使用
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;
- 代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每一种方法都进行代理,静态代理在程序规模稍大时就无法胜任了。
- 如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度
5. 适用场景
- 远程代理,为一个不同地址空间或网络上的对象提供本地代表。例如,用户申请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空间。
- 虚拟代理,这种方式通常用于要创建的目标对象开销很大时。例如,下载一幅很大的图像需要很长时间,因某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。
- 安全代理,这种方式通常用于控制不同种类客户对真实对象的访问权限。
- 智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,当一个对象被引用时,提供一些额外的操作,比如将对此对象调用的次数记录下来。
- 延迟加载,指为了提高系统的性能,延迟对目标的加载。
6. 模式拓展
6.1 iOS开发中delegate和代理模式的区别
在iOS开发中我们经常会提到使用代理实现两个类间的通讯,这个代理并非这边的代理设计模式。为了便于区分称为委托可能更加合适。
委托模式角色组成可以划分为:
- 委托协议(DelegateProtocol): 定义被委托者需要实现的接口协议
- 被委托者(Delegatee): 接受委托角色,需要实先委托协议接口
-
发起委托者(Subject): 通过调研委托协议的接口,与被委托者进行通讯
UML关系图如下:
注释:被委托者与发起委托者类关系,也有可能是关联关系,这边以依赖关系为例
6.2 动态代理
对于前面说的代理模式也称为 静态代理,代理主题对真实主题进行了包含引用。这就会导致真实主题和代理主题必须一一对应,增加真实主题的话也必须增加代理主题。设计代理前,真实主题就必须已经存在了。针对这个问题,代理模式拓展出了 动态代理
6.2.1 什么是动态代理
定义:运用程序语言的动态特性,在程序运行时确认代理类和真实主题类的代理关系。动态代理属于AOP(面向切面编程)的一种思想
6.2.2 Object-C动态代理NSProxy
Object-C 官方接口中提供了NSProxy,可以进行动态代理,其原理是进行消息转发。NSProxy属于抽象类,使用的时候需要继承它,并对
-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel 和 -(void)forwardInvocation:(NSInvocation *)invocation 进行实现。
6.2.3 代码实例
我们以游戏代理有例。国内很多游戏公司,都会去代理国外比较好的游戏。一方面国内的游戏公司不用重新开发游戏,国外的游戏厂商也不用关心游戏在国内的推广。考虑到网络等因素本国玩家也能得到更好的游戏体验。
腾讯作为国内的游戏巨头,也代理了一些比较出名的游戏。
当年腾讯代理的地下城与勇士(DidNotFinish)可谓火爆大街小巷,玩家进行游戏时走的就是腾讯代理(TXGameProxy)的渠道。代理优秀的游戏总是多多益善的,这不皇室战争(ClashRoyale)也走了腾讯代理。可以预知未来还有更多的游戏会走腾讯代理,这个时候动态代理的方式就很适合了。不论你玩的什么游戏,只要是它代理的,就直接走他的统一代理就好了,你只要告诉他具体游戏,大家走统一标准(GameAgentProtocol)就行了。
UML关系图
- 定义游戏统一接口协议GameAgentProtocol
// GameAgentProtocol.h
#import <Foundation/Foundation.h>
@protocol GameAgentProtocol <NSObject>
- (void)login;
- (void)logout;
- (void)topUp:(CGFloat)money;
@end
- 定义动态游戏代理TXGameProxy
//TXGameProxy.h
#import <Foundation/Foundation.h>
#import "GameAgentProtocol.h"
@interface TXGameProxy : NSProxy<GameAgentProtocol>
- (id)initWithTarge:(id<GameAgentProtocol>)target;
@end
// TXGameProxy.m
#import "TXGameProxy.h"
@interface TXGameProxy()
@property (nonatomic, strong) id<GameAgentProtocol> target;
@end
@implementation TXGameProxy
// NSProxy 本身是没有init方法的
- (id)initWithTarge:(id<GameAgentProtocol>)target {
_target = target;
return self;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
if (_target && [_target respondsToSelector:invocation.selector]) {
[invocation invokeWithTarget:_target];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
if ([_target isKindOfClass:[NSObject class]]) {
return [(NSObject *)_target methodSignatureForSelector:sel];
}
return [super methodSignatureForSelector:sel];
}
//如果需要可以对代理方法进行拓展下
- (void)topUp:(CGFloat)money {
NSLog(@"如果未满18岁,请在父母同意下,进行充值");
if (_target && [_target respondsToSelector:@selector(topUp:)]) {
[_target topUp:money];
}
}
@end
- 定义具体游戏类ClashRoyale和DidNotFinish
//DidNotFinish.h
#import <Foundation/Foundation.h>
#import "GameAgentProtocol.h"
@interface DidNotFinish : NSObject<GameAgentProtocol>
@end
//DidNotFinish.m
#import "DidNotFinish.h"
@implementation DidNotFinish
- (void)login {
NSLog(@"Player login DNF");
}
- (void)logout {
NSLog(@"Player logout DNF");
}
- (void)topUp:(CGFloat)money {
NSLog(@"Player top up %lf DNF", money);
}
@end
//ClashRoyale.h
#import <Foundation/Foundation.h>
#import "GameAgentProtocol.h"
@interface ClashRoyale : NSObject<GameAgentProtocol>
@end
//ClashRoyale.m
#import "ClashRoyale.h"
@implementation ClashRoyale
- (void)login {
NSLog(@"Player login ClashRoyale");
}
- (void)logout {
NSLog(@"Player logout ClashRoyale");
}
- (void)topUp:(CGFloat)money {
NSLog(@"Player top up %lf ClashRoyale", money);
}
@end
- 调用测试
+ (void)test1 {
TXGameProxy *proxy = [[TXGameProxy alloc] initWithTarge:[DidNotFinish new]];
[proxy login];
[proxy topUp:60];
[proxy logout];
TXGameProxy *proxy1 = [[TXGameProxy alloc] initWithTarge:[ClashRoyale new]];
[proxy1 login];
[proxy1 topUp:80];
[proxy1 logout];
}
运行结果
6.2.4 分析
- 动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到NSProxy一个集中的方法中处理。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
- 新增一个同类型的真实主题时,不需要重新增加一个主题。
6.2.5 适用范围
- 模拟多继承
- 进行解决弱引用问题(YYWeakProxy)
- AOP切片编程