iOS设计模式-代理模式

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),有购买什么产品的需要直接找他们就行了,他们通过他们的渠道帮忙到海外商店进行购买你要的产品,然后发货给你。他们从中赚取了佣金,而我们则节约了成本。

  1. 首先定义商店售货的接口(抽象主题)
// StoreInterface.h
#import <Foundation/Foundation.h>

@protocol StoreInterface <NSObject>
- (void)sale:(NSString *)product;
@end
  1. 定义海外商店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
  1. 定义代购类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
  1. 调用测试,购买找代购购买奶粉
+ (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关系图


  1. 定义游戏统一接口协议GameAgentProtocol
// GameAgentProtocol.h
#import <Foundation/Foundation.h>

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