iOS——协议与分类

在OC语言中有一项特性叫做协议(protocol),由于OC不支持多重继承,因而我们把某个类应该实现的一系列方法定义在协议里。协议可以很好的描述接口。

** 分类(Category)**也是OC的一种重要特性,利用分类可以直接为当前类添加方法,无需通过继承子类,契合OC语言运行期系统是高度动态的。

23:通过委托与数据源协议进行对象间通信

对象之间经常需要相互通信,而通信方式有很多种。Objective-C开发者广泛使用一种名叫"委托模式"(Delegate pattern)的编程设计模式来实现对象间的通信,该模式的主旨是:定义一套接口,某对象若想接受另一个对象的委托,则需遵从此接口,以便成为其"委托对象"(delegate)。而这"另一个对象"则可以给其委托对象回传一些信息,也可以在发生相关事件时通知委托对象。

此模式可将数据与业务逻辑解耦。比方说,用户界面里有个显示一系列数据所用的视图,那么,此视图只应包含显示数据所需的逻辑代码,而不应决定要显示何种数据以及数据之间如何交互等问题。视图对象的属性中,需要包含负责数据与事件处理的对象。这两种对象分别称为"数据源"(data source)与"委托"(delegate)。

下面举个例子:假设要便携一个从网上获取数据的类,如果网络请求时间过长,导致阻塞应用程序,这是很糟糕的,所以我们会采用委托模式.

委托模式

首先使用的这个类含有一个**“委托对象”**,在请求完数据之后,它会回调这个对象。下面是回调委托对象的过程:
在这里插入图片描述
EOCNetworkFetcher对象就是我们完成网络请求的类。
EOCDataModel对象就是 EOCNetworkFetcher的委托对象。EOCDataModel请求 EOCNetworkFetcher以异步方式执行一项任务"(perform a task asynchronously),而 EOCNetworkFetcher在执行完这项任务之后,就会通知其委托对象,也就是 EOCDataModel。
这里所谓的异步方式:即GCD中一种添加任务的方法:添加后,不必等待任务结束,函数会立即返回。推荐优先使用这种方式,因为它不会阻塞其他任务的执行。
下面看看代码:

@protocol EOCNetworkFetcherDelegate

- (void)networkFetcher:(EOCNetworkFetcher*) fetcher
didReceiveData:(NSData*)data;- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didFailWithError:(NSError*)error;


@end

协议名通常是相关类名后加上Delegate。有了协议之后类就可以用一个属性来存放委托对象了。

@property (nonatomic, weak) id <EOCNetworkFetcherDelegate> delegate;

一定要注意∶这个属性需定义成weak,而非strong,因为两者之间必须为"非拥有关系"(nonowning relationship)。通常情况下,扮演 delegate 的那个对象也要持有本对象。例如在本例中,想使用EOCNetworkFetcher 的那个对象就会持有本对象,直到用完本对象之后,才会释放。假如声明属性的时候用strong将本对象与委托对象之间定为"拥有关系",那么就会引入"保留环"(retain cycle)。因此,本类中存放委托对象的这个属性要么定义成 weak,要么定义成 unsafe unretained,如果需要在相关对象销毁时自动清空,则定义为前者,若不需要自动清空,则定义为后者。
下图解释了他们的所有权关系。
在这里插入图片描述
实现委托对象的办法是声明某个类遵从委托协议,然后把协议中想实现的那些方法在类里实现出来。某类若要遵从委托协议,可以在其接口中声明,也可以在"class-continuation 分类",如果要向外界公布此类实现了某协议,那么就在接口中声明,而如果这个协议是个委托协议的话,那么通常只会在类的内部使用。所以说,这种情况一般都是在"class-continuation分类"里声明的∶

@implementation EOCDataModel () <EOCNetworkFetcherDelegate>
@end@implementation EOCDataModel


- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
didReceiveData:(NSData*)data {/* Handle data */}-(void)networkFetcher:(EOCNetworkFetcher*)fetcher didFailWithError:(NSError*)error {/* Handle error */


}@end

协议中的方法一般都是“可选的(optional)”。为了指明可选方法,委托协议经常使用@optional关键字来标注。

@protocol EOCNetworkFetcherDelegate
@optional
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didReceiveData:(NSData*)data;
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didFailWithError:(NSError*)error;
@end



如果在委托对象上调用可选方法,那么必须提前使用类型信息查询方法判断这个委托对象能否响应相关选择子,如下:

NSData *data = /*data obtained from network*/;
if ([_delegate respondToSelector:@selector(networkFetcher:didReceiveData:)]) {
	[_delegate networkFetcher:self didReceiveData:data];
}

这段代码调用了“respondsToSeclector: ”,来判断是否实现相关方法,如果返回false则不会进行任何操作。
delegate对象中的方法名也一定要起的恰当。方法名应该准确描述当前发生的事件以及delegate对象为何要获知此事件。此外,在调用delegate对象中的方法时,总是应该把发起委托的实例也一并传入方法中,这样,delegate对象在实现相关方法时,就能根据传入的实例分别执行不同的代码了。

- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didReceiveData:(NSData*)data {
	if (fetcher == _myFetcherA) {
		/*Handel data*/
	} else {
		/*Handel data*/
	}
}


这个参数可以用来判断到底是哪个实例对象获取数据。
delegate里的方法也可以用于从获取委托对象中获取信息。比如说,MyNetworkFetcher类也许想提供一种机制:在获取数据时如果遇到了“重定向”(redirect),那么将询问其委托对象是否应该发生重定向。delegate对象中的相关方法可以写成了这样:
重定向:就好比我们找一个A广告公司给设计名片,A明确告诉我们他们不会设计,就让我们找B公司,结果B公司给我设计好了,所以我们会对外宣称是B公司给我们设计的名片,(所以我们就相当于发送了两次次请求,URL地址栏里就从A变成了B公司)

- (BOOL)networkFetcher:(EOCNetworkFetcher*)fetcher shouldFollowRedirectToURL:(NSURL*)url;


通过这个例子,大家应该很容易理解此模式为何叫做“委托模式”:因为对象把应对某个行为的责任委托给另外一个类了。

协议定义接口

用协议定义一套接口:令某类经由该接口获取其所需的数据。委托模式的这一用法旨在向类提供数据,故而又称“数据源模式”(Data Source Pattern)在此模式中,信息从数据源(Data Source)流向类(Class);而在常规的委托模式中,信息则从类流向受委托者(Delegate)。
在这里插入图片描述
比方说,用户界面框架中的“列表视图”(list view)对象可能会通过数据源协议来获取要在列表中显示的数据。除了数据源之外,列表视图还有一个受委托者,用于处理用户与列表的交互操作。将数据源协议与委托协议分离,能使接口更清晰,因为这两部分的逻辑代码也分开了。另外,“数据源”与“受委托者”可以是两个不同的对象。然而一般情况下,都用同一个对象来扮演这两种角色。
在实现委托模式与数据源模式时,如果协议中的方法是可选的,那么就会写出一大批类似下面的代码来:

if ([_delegate respondToSelector:@selector(someClassDidSomething)]) {
	[_delegate someClassDidSomething];
}


使用这种方法很容易查出某个委托对象是否能响应特定的选择子,但除了第一次检测的结果外,后续的检测可能是多余的,鉴于此,我们通常把委托协议能否响应这一信息存起来,以优化程序性能。
假设在"网络数据获取器"那个例子中,delegate 对象所遵从的协议里有个表示数据获取进度的回调方法,这个方法在网络数据获取器的生命期(life cycle)里会多次调用。
将刚才说的那个选择子加入之后,delegate 对象所要实现的委托协议就扩充成∶

@protocol EOCNetworkFetcherDelegate
@optional
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didReceiveData:(NSData*)data;
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didFailWithError:(NSError*)error;
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didUpdateProgressTo:(float)progress;
@end


扩充后增加了一个方法networkFetcher:didUpdateProgressTo::将方法响应能力缓存起来的最佳途径就是使用“位段”(bitfield)数据类型。这是一项乏人问津的C语言特性,但在此处正合适。我们可以把结构体中某个字段所占用的二进制个位数设为特定的值,
详情可以参考位段
例如:

struct data {
	unsigned int fieldA : 8;
	unsigned int fieldB : 4;
	unsigned int fieldC : 2;
	unsigned int fieldD : 1;
};

上面成员后的:整数,定义了该成员占用几个位,fieldD占用一个位,只可以表示0 1,我们可以像这样缓存是否实现方法,如下:

@interface EOCNetworkFetcher() {
	struct {
		unsigned int didReceiveData : 1;
		unsigned int didFailWithError : 1;
		unsigned int didUpdateProgressTo : 1;
	} _delegateFlags;
}
@end


我们创建了一个结构体里面只有几个占有一位的位段,就可以存储许多bool值,这个结构体用来缓存委托对象是否能响应特定的选择子。实现缓存功能的代码写在delegate属性所对应的设置方法里:

- (void)setDelegate:(id<EOCNetworkFetcher>)delegate {
	_delegate = delegate;
	_delegateFlags.didReceiveData = [delegate respondToSelector:@selector(didReceiveData:)];
	_delegateFlags.didFailWithError = [delegate respondToSelector:@selector(didFailWithError:)];
	_delegateFlags.didUpProgressTo = [delegate respondToSelector:@selector(didUpProgressTo:)];
}


在相关方法要调用很多次时候,就值得这种优化。比如多次通过数据源协议从数据源获取多个相互独立的数据。

24:将类的实现代码分散到便于管理的数个分类中

类中经常容易填满各种方法,而这些方法的代码则全部堆在一个巨大的实现文件中,有时这么做是合理的,因为即使通过重构把这个类打散,效果也不会更好。在此情况下,可以通过“分类”机制,把类代码按逻辑划入几个分区中。
比如我们把个人信息建模为类,那么就可能有以下方法:

#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends;

- (id)initWithFirstName:(NSString*)name andLastName:(NSString*)lastName;

//Friendship
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendWith:(EOCPerson*)person;

//Work
- (void)performDaysWork;
- (void)takeVacationFromWork;

//Play
- (void)goToTheCinema;
- (void)goToSportsGame;

@end


如果把所有方法都放在一个类里面,那么会导致源代码文件越来越大,难于管理,我们可以利用分类机制改写上面的代码:

#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject <NSCopying>

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends;

- (id)initWithFirstName:(NSString*)name andLastName:(NSString*)lastName;

@end

@interface EOCPerson (Friendship)
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendWith:(EOCPerson*)person;
@end

@interface EOCPerson (Work)
- (void)performDaysWork;
- (void)takeVacationFromWork;
@end

@interface EOCPerson (Play)
 - (void)goToTheCinema;
 - (void)goToSportsGame;
@end


现在通过方法特性将方法分成了好几个部分,所以说这种特性叫分类,原则上是将基本要素声明在“主实现”,其余的归于其他分类。
使用分类机制之后,依然可以把整个类都定义在一个接口文件中,并将其代码写在一个实现文件里。**可是,随着分类数量的增加,当前这份实现文件很快就膨胀得无法管理了。**此时可以把每个分类提取到各自的文件中去。以EOCPerson为例,可以按照其分类拆开分成下列几个文件:

//EOCPerson+Friendship.h
#import "EOCPerson.h"

@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendWith:(EOCPerson*)person;

@end

//EOCPerson+Friendship.m
#import "EOCPerson+Friendship.h"

@implementation EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person {
    //...
}
- (void)removeFriend:(EOCPerson*)person {
    //...
}
- (BOOL)isFriendWith:(EOCPerson*)person {
    //...
}

@end


通过分类机制,可以把类代码分成很多个易于管理的小块,以便单独检视。使用分类机制之后,如果想用分类中的方法,要记得不仅引入主文件并且要一并引入分类的头文件。
即使类本身不是太大,我们也可以使用分类机制将其切割成几块,把相应的代码归入不同的“功能区”(functional area)中。
之所以要将类代码打散到分类中还有个原因,就是便于调试:对于某个分类中的所有方法来说,分类名称都会出现在其符号中。例如,“addFriend:”方法的“符号名”(symbol name)如下

-[EOCPerson(Friendship) addFriend:]


根据回溯的分类名称,很容易就能定位到类方法所属的功能区,比如我们的私有方法,使用者可以在回溯信息发现private一词,从而不直接调用这个方法。这可以算是一种编写“自我描述式代码”(self-documenting code)的方法。

25:总是为第三方类的分类名称加前缀

分类机制通常用于向无源码的既有类中新增功能。这个特性极为强大,分类中的方法是直接添加在类里面的,它们就好比这个类中的固有方法。将分类方法加入类中这一操作是在运行期系统加载分类时完成的。运行期系统会把分类中所实现的每个方法都加入类的方法列表中。如果类中本来就有此方法,而分类又实现了一次,那么分类中的方法会覆盖原来那一份实现代码。实际上可能会发生很多次覆盖,比如某个分类中的方法覆盖了"主实现"中的相关方法,而另外一个分类中的方法又覆盖了这个分类中的方法。多次覆盖的结果以最后一个分类为准

所以我们可以给相关名称加上共用前缀:

@interface NSString(ABC_HTTP)

- (NSString*)abc_urlEncodedString;
- (NSString*)abc_urlDecodedString;

@end


■ 向第三方类中添加分类时,总应给其名称加上你专用的前缀。
■ 向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀。

26:勿在分类中声明属性

属性是封装数据的方式。尽管从技术上说,分类里也可以声明属性,但这种做法还是要尽量避免。原因在于,除了"class-continuation分类"之外,其他分类都无法向类中新增实例变量,因此,它们无法把实现属性所需的实例变量合成出来。
例如把之前的属性朋友写入分类:

@interface EOCPerson (Friendship)
@property (nonatomic, strong) NSArray *friends;

- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendWith:(EOCPerson*)person;

@end


这时编译器会给出警告信息,warning: property ‘friends’ requires method’friends’ to be defined - use @dynamic or provide a method implementation in this category [-Wobjc-property-implementation ]
warning: property ‘friends’ requires method’setFriends:'to be defined - use @dynamic or provide a method implementation in this category [-Wobjc-property-implementation]。
这段话的大致意思是说此分类无法合成与 friends 属性相关的实例变量,所以开发者需要在分类中为该属性实现存取方法。此时可以把存取方法声明为@dynamic,也就是说,这些方法等到运行期再提供,编译器目前是看不见的。如果决定使用消息转发机制在运行期拦截方法调用,并提供其实现,那么或许可以采用这种做法。
关联对象能够解决在分类中不能合成实例变量的问题。比方说。我们可以在分类中用下面这段代码实现存取方法∶

#import<objc/runtime.h>

static const char *kFriendsPropertyKey = "kFriendsPropertyKey";@implementation EOCPerson (Friendship)


- (NSArray*)friends {

	
return objc getAssociatedObject(self,kFriendsPropertyKey);}- (void)setFriends:(NSArray*) friends {objc_setAssociatedObject(self,
kFriendsPropertyKey, friends,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);


	}@end

这样做可行,但不太理想。要把相似的代码写很多遍。而目在内存管理问题上容易出错。因为我们在为属性实现存取方法时,经常会忘记遵从其内存管理语义.
正确做法是把所有属性都定义在主接口里。类所封装的全部数据都应该定义在主接口中。这里是唯一能够定义实例变量(也就是数据)的地方。而属性只是定义实例变量及相关存取方法所用的"语法糖",所以也应遵循同实例变量一样的规则。至于分类机制,应该理解为一种手段,目的扩展类的功能,并非封装数据。
最后,有时候只读属性还是可以在分类中使用的。

27:使用分类隐藏实现细节

类中经常会包含一些无须对外公布的方法及实例变量。OC动态消息系统的工作方式决定了其不可能实现真正的私有方法或私有实例变量。然而,我们最好还是只把确实需要对外公布的那部分内容公开。那么,这种不需对外公布但却应该具有的方法及实例变量应该怎么写呢?这个时候,这个特殊的“class-continuation分类”就派上用场了。
这个分类和普通分类不同,它必须定义在其所接续的类的实现文件中,这时唯一能声明实例变量的分类,并且此分类没有特定的实现文件,所有方法都应该定义在类的主实现文件中,且这个类没有名字

@interface EOCPerson ()
//...
@end


为什么需要这种分类呢?因为其中可以定义方法和实例变量。为什么能在其中定义方法和实例变量呢?只因为有“稳固的ABI”这一机制,使得我们无须知道对象大小即可使用它。由于类的使用者不一定需要知道实例变量的内存布局,所以,它们也就未必要定义在公共接口中了。基于上述原因,我们可以像在类的实现文件里那样,于“class-continuation分类”中给类新增实例变量,只需要在适当位置加几个括号,加入实例变量:

@interface EOCPerson () {
    NSString *_anInstanceVariable;
}

@end

@implementation EOCPerson {
    int _anotherInstanceVariable;
}



这样做的好处:只供本类使用,因为即使在公共接口标注为private还是会泄露实现细节,通过这种方法可以隐藏起来。
==实例变量也可以定义在实现块里,从语法上说,这与直接添加到"class-continuation 分类"等效,只是看个人喜好了。==建议将其添加在"class-continuation分类"中,以便将全部数据定义都放在一处。
由于"class-continuation分类"里还能定义一些属性,所以在这里额外声明一些实例变量也很合适。这些实例变量并非真的私有,因为在运行期总可以调用某些方法绕过此限制,不过,从一般意义上来说,它们还是私有的。此外,由于没有声明在公共头文件里,所以将代码作为程序库的一部分来发行时,其隐藏程度更好。

编写Objective-C++代码时"class-continuation分类"也尤为有用。Objective-C++是Objective-C与C++的混合体,其代码可以用这两种语言来编写。由于兼容性原因,游戏后端一般用C++来写。另外,有时候要使用的第三方库可能只有 C++绑定,此时也必须使用C++来编码。在这些情况下,使用"class-continuation分类"会很方便。
"class-continuation分类"还有一种合理用法,就是将 public接口中声明为"只读"的属性扩展为"可读写",以便在类的内部设置其值。我们通常不直接访问实例变量,而是通过设置访问方法来做,因为这样能够触发KVO通知,其他对象有可能正监听此事件。出现在"class-continuation分类"或其他分类中的属性必须同类接口里的属性具备相同的特质,不过,其"只读"状态可以扩充为"可读写"。例如,有个描述个人信息的类,其公共接口如下∶

@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@end


我们可以在class- continuation分类中把这两个属性扩展:

@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end


现在,EOCPerson的实现代码可以随意调用“setFirstName:”或“setLastName:”这两个设置方法,也可以使用点语法设置属性。这样,封装在类中的数据就由实例本身来控制,而外部代码无法修改其值。注意,若观察者正在读取属性值而内部代码又在写入该属性时,则有可能引发“竞争条件”(race condition)。合理使用同步机制可以缓解此问题。
只会在类的实现代码中才会用到的方法也可以声明在“class-continuation分类”中,这么做比较合适。这里可以向前声明一下,可以把类含有的相关方法都统一描述于此。
最后还有一个用法:若对象遵从的协议只应视为私有,则可以在“class-continuation”中声明,比如说某个协议在私有API中:


#import "EOCPerson.h"
#import "EOCSecretDelegate.h"
@interface EOCPerson () <EOCSecretDelegate>

@end@implementation EOCPerson 
/* .·.*/

@end


这样就可以不为外界所知了。

28:使用协议提供匿名对象

我们可以利用协议把自己API中的实现细节隐藏起来,将返回的对象设计为遵从此协议的id类型。这样便可以隐藏类名,若是接口后有很多不同的类,而你不想指明使用哪个类,可以使用此方法。此概念经常称为隐匿对象,例如之前我们在委托协议中的“受委托者”:

@property (nonatomic, weak) id <EOCDelegate> delegate;


任何类的对象都可以充当这一属性,主要遵循EOCDelegate协议就行。如果需要检查类,可以在运行期查出。
NSDictionary也能实际说明这一点:在字典中,键的标准内存管理语义是“设置时拷贝”,而值的语义是“设置时保留”。因此,在可变字典中,设置键值对应的方法的签名是

- (void)setObject:(id)object forKey:(id<NSCopying>)key


表示键的那个参数类型是id<NSCopying>,它可以是任何类型,只要遵从NSCopying协议就好,这样的话,就能向该对象发送拷贝信息了。这个key参数可以视为匿名对象。与delegate属性一样,字典也不关心key对象所属的具体类。而且它也决不应该依赖于此。字典对象只要能确定它可以给此实例发送拷贝信息就行了。
处理数据库连接(detabase connection)的程序库也用这个思路,以匿名对象来表示从另一个库中所返回的对象。对于处理连接所用的那个类,你也许不想叫外人知道其名字,因为不同的数据库可能要用不同的类来处理。如果没办法令其都继承自同一基类,那么就得返回id类型的东西了。不过我们可以把所有数据库连接都具备的那些方法放到协议中,令返回的对象遵从此协议。协议可以这样写:

@protocol EOCDatabaseConnection
- (void)connect;
- (void)disconnect;
- (BOOL)isConnected;
- (NSArray*)performQuery:(NSString*)query;
@end


然后就可以用“数据库处理器”(database handler)单例来提供数据库连接了。这个单例的接口可以写成:

#import <Foundation/Foundation.h>
@protocol EOCDatabaseConnection;

@interface EOCDatabaseManager : NSObject

+ (id)sharedInstance;
- (id<EOCDatabaseConnection>)connectionWithIdentifier:(NSString*)identifier;

@end


这样就不会泄漏链接类的名称了,使用此API的人仅仅要求所返回的对象能用来连接、断开并查询数据库即可。
有时候对象类型并不重要,重要的是对象可以响应定义在协议里的特定方法。在这个情况下,也可以用这些“匿名类型”(anonymous type)来表达这一概念。即便实现代码总是使用固定的类,你可能还是会把它写成遵从某协议的匿名类型,以表示类型在此处并不重要。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值