iOS 经典的协议委托模式

前言

@interface AViewController : <UITableViewDelegate, UITableViewDataSource>
@end

...
self.table.delegate = self;
self.table.datasource = self;

是否经常在开发过程中使用到列表?是否经常为控制器设置遵循的协议,并在相应的方法中提供数据或提供数据呢?实际上列表的这种设计采用得就是协议委托模式。上述代码中,协议 UITableViewDelegateUITableViewDataSource 是列表所需要的关于cell的数量、样式、高度等等,这些功能不应由内部实现,只能由外部使用者提供,因此将其抽出来形成协议,交由那些委托对象来实现。这里的委托者就是 self即当前的控制器。

在开发过程中对某些功能进行封装时,经常要考虑的就是如何和外部进行信息的交互。常用的手段很多,如 匿名函数block,这种方式保留了 C 的风格,OC 中与之相似的就是协议委托模式。

接下来我们一步一步的深入了解什么是协议委托模式。为了能够通俗的理解,我下面会生活中的例子来进行讲解。

有一天,我想吃些水果,该如何呢?那么我可能需要找到一个水果店进行水果的挑选和购买。

被委托者

我们分别创建人和水果店的类 PersonFruitShop,并创建实例表示我和水果店这两个对象。

// 水果店定义
@interface FruitShop : NSObject
/// 购买苹果
-(CGFloat)buySomeAppleWithMoney:(CGFloat)money;
@end
@implementation FruitShop
/// 购买苹果
-(CGFloat)buySomeAppleWithMoney:(CGFloat)money{
    return money / 10.98;
}
@end

// 人个体
#import "FruitShop.h"
@interface Person : NSObject
@property (nonatomic, strong) FruitShop* shop; //水果店
@end
@implementation Person
@end

水果店具有贩卖水果的功能。
我需要水果店购买苹果,所以要对水果店的进行引用操作,为了能够进行水果的购买操作。

self.me = Person.new;
self.shop = FruitShop.new;
// 对水果店进行引用
self.me.shop = self.shop;

现在我们需要触发购买操作,我们给 Person 添加购买方法,以便触发购买。

@implementation Person
-(void)buyApple{
    // 如果找到水果店,并且该水果店能相应购买需求
    if (self.shop && [self.shop respondsToSelector:@selector(buySomeAppleWithMoney:)]) {
        // 购买水果
        CGFloat number = [self.shop buySomeAppleWithMoney:100];
        NSLog(@"买到了 %.2f 斤苹果", number);
    }
}
@end

外部触发购买的事件。

[self.me buyApple];
买到了 9.11 斤苹果

我们成功通过水果店购买到了 9.11 斤苹果。

当自身不具备某种功能时,可以通过引入另一种类型来提供相关的能力,新引入的对象就是被委托者,换句话说,我没有能提供水果的能力,但是我又需要水果,那么我通过属性注入的方式引入一个具有提供水果的能力水果店,通过水果店来为我产生水果,而这个水果店对象就是被委托对象。

问题一:循环引用

一般来说上述例子没有什么问题,但是在一些特定的应用场景下就会出现问题。学 OC 的人想必都知道 OC 的内存管理机制,其中有一个需要特别注意的问题就是循环引用,无论是相互持有还是单向循环持有都会无法释放。

在本类中,meshop 只是单向持有,因此不用担心无法释放的问题,但是如果在 me 持有 shop 的之前,shop 已经持有了 me 呢?但是似乎没有什么理由让 shop 持有 me ,但是我们回到 UITableView 示例中,通常情况下,控制器类会先持有一个列表视图,而列表视图的 delegatedatasource 又引用到了 self 即当前类,那么就会出现循环引用的问题,可是为什么没有出现内存泄漏的问题呢?这是 OC 中 weak 修饰符 所起到的作用,该修饰符引用对象但是不会让对象计数器加一,在可能存在的循环链中如果有一方采用了该修饰符,那么循环链就不存在了。

delegatedatasource 指向其他对象而非 self 同样可以避免循环引用问题,只不过系统在设计时已经考虑到这些错误的使用情况,因此采用了 weak 修饰符 解决很大可能性的循环引用问题。

注:无论何种方式,只要避免单向循环引用链的出现就能避免这样的内存泄漏问题。

问题二:硬编码

水果店的例子中还会存在一个问题,并不是只有水果店才能卖给我水果,如果我遇到了一家超市,该超市同样能提供水果的能力,那我还需要继续寻找水果店吗?当然不需要了,但是会有新的问题出现。

#import "FruitShop.h"
@interface Person : NSObject
@property (nonatomic, strong) FruitShop* shop;
-(void)buyApple;
@end

我们发现,在 Person 类中,我们指定了 FruitShop 类,即水果店,这就意味着引用过来的对象应该是 FruitShop 类或者其子类,否则我们可能无法购买,但是超市很大可能并不是水果店的子类,或者购买方法并不是 buySomeAppleWithMoney:。那我们再引入一个新的 Supermarket 超市类? 继续引入新类型无法解决根本问题。

我们知道,OC 中有一个 id 类型,该类型表示任何类型。我们可以使用它来代替,但是下一个问题又随之而来,如果提供对象之间针对同一种功能有着不同的方法声明,那么这个 id 对象如何调用正确的方法呢?解决方案就是协议

协议

协议就是一组方法的声明。类似 Java 中接口,只有方法的声明,没有相关的实现。协议的作用是用来约束类型的,当某个类声称遵循了协议,那么协议中标记@required 的方法就需要进行实现。
换个角度来看,协议可以为类添加更多的功能:当自定义对象遵循并实现了 NSCopying 协议,那么这个对象就能够通过 [obj copy] 方法复制一个新对象;当自定义对象遵循并实现了 NSCoding 协议,那么这个对象就能被归档和解档。

回到水果店问题,我们通过协议来约定 Person 类购买水果的相关能力。

创建协议

在协议中声明购买水果的方法。

@protocol MyBuy <NSObject>
@required
/// 购买苹果
-(CGFloat)buySomeAppleWithMoney:(CGFloat)money;
@end

我们对 Person 进行改造。

#import "MyBuy.h"
@interface Person : NSObject
@property (nonatomic, weak) id <MyBuy> shop; //遵循协议的任意对象
-(void)buyApple;
@end

@implementation Person
-(void)buyApple{
    // 如果找到水果店,并且该水果店能相应购买需求
    if (self.shop && [self.shop respondsToSelector:@selector(buySomeAppleWithMoney:)]) {
        // 购买水果
        CGFloat number = [self.shop buySomeAppleWithMoney:100];
        NSLog(@"买到了 %.2f 斤苹果", number);
    }
}
@end

这样,只要 Person 遵循实现协议 MyBuy 中购买水果的能力,就能够完成买水果的操作。

我们接着让水果店遵循并实现该协议。

#import "MyBuy.h"
@interface FruitShop : NSObject <MyBuy>
@end

@implementation FruitShop
/// 购买苹果
-(CGFloat)buySomeAppleWithMoney:(CGFloat)money{
    return money / 10.98;
}
@end

FruitShop 类删除购买方法的声明,直接实现 MyBuy 协议中的购买方法即可。

// 对水果店进行引用
self.me.shop = self.shop;
[self.me buyApple];

外部虽然没有什么变化,但是性质完全不一样,现在只要其他的类,如 Supermarket,同样遵循并实现协议 MyBuy 就可以直接替换掉 shop

self.me.shop = self.supermarket;
[self.me buyApple];

这样就解决了硬编码问题,同时,当我遇到一家超市而不是水果店时,我依然可以获取到水果啦。

总结

协议委托模式是 OC 中经典的设计模式,该模式在一定程度上降低了代码的耦合性。同时协议委托模式提高了代码的灵活性,也解决了委托者和被委托者的通信问题。


协议的应用

前文提到,协议是一种类型约束。在 iOS 开发过程中,你会经常接触。这里我列举了一些常用到的情景。

  • 添加功能

协议可以被看作为类添加功能的一种方式,通过协议我们能清楚的知道类具有哪些能力。协议也方便我们管理类,当我们想移除类遵循的协议时,可以通过查询协议找出对应的方法进行移除即可。

  • 约束类、类型

协议可以用来约束类和类型,常见情况如下。

// 约束类
// 该类需要实现 NSCoding 协议的 @required 方法
@interface Person : NSObject <NSCoding>
@end

// 约束对象
// 虽说是id类型,但前提条件是必须遵循协议
@property (nonatomic, weak) id <MyBuy> shop;
// 必须是字符串数组
@property (nonatomic, strong) NSArray <NSString*>* list;
// value 必须是 Student
@property (nonatomic, strong) NSDictionary <NSString*,Student*>* list;
  • 添加属性

协议中是可以添加属性的,但是需要使用 synthesize 关联成员变量。

@protocol MyBuy <NSObject>
@property (nonatomic, assign) CGFloat price;
@end

@interface FruitShop : NSObject <MyBuy>
@end
@implementation FruitShop
@synthesize price = _price; //关联成员变量
@end
  • 面向接口编程

也就是面向协议编程,对外声明协议,将必要的属性、方法声明在协议中,外部通过某种手段通过协议取得该对象。

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

推荐阅读更多精彩内容

  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,152评论 30 470
  • 本文来源:www.jianshu.com/p/67293140c570 相信提起代理(delegate),无论你是...
    我家的小鲤鱼阅读 3,727评论 1 8
  • 在项目中我们经常会用到代理的设计模式,这是iOS中一种消息传递的方式,也可以通过这种方式来传递一些参数。...
    张战威ican阅读 856评论 0 8
  • “只有紫霞真正离开至尊宝的时候,至尊宝才会变成孙悟空。” 这是前任3里诠释的最好一句话。 与其相互折磨,不如彼此放...
    叫我晨社长阅读 388评论 0 0
  • 明明才是二十左右的年纪,却仿佛对大部分事情都失去了激情,想要交朋友却在想,算了,反正都错过了这么多人.想着手一些...
    小半于你阅读 182评论 0 0