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]; //操作协议中的方法或属性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值