【Effective Objective - C】—— 协议与分类
- 23.通过委托与数据源协议进行对象间通信
- 协议
- 委托模式
- 数据源模式
- 要点
- 24.将类的实现代码分散到便于管理的数个分类之中
- 要点
- 25.总是为第三方类的分类名称加前缀
- 要点
- 26.勿在分类中声明属性
- 要点:
- 27.使用 “class-continuation 分类” 隐藏实现细节
- class-continuation分类:
- “class-continuation分类”的合理用法:
- 要点
- 28.通过协议提供匿名对象
- 要点
- 总结
23.通过委托与数据源协议进行对象间通信
在OC语言里,委托模式是对象之间进行相互通信的主要方法之一。该模式是定义一套接口,某个对象需要接受另一个对象的委托的时候,需要遵从这个接口,就成为了其委托对象。
具体实现可以把对象分为两种:数据源和委托
协议
委托模式
OC语言一般通过协议来实现委托模式,委托协议的命名采取驼峰命名法,一般在名词之后添加Delegate这个单词。
协议实现的过程如下:
定义协议的时候需要注意:属性的修饰不能用strong,需要weak或者unsafe_unretained,因为本对象和委托对象之间的关系是非拥有关系。 如果采用了strong修饰则会出现本对象和委托对象之间的拥有关系,存在保留环。
如果需要相关对象销毁的时候字典清空则用weak,若不要自带情况则unsafe_unretained。
某类若要遵从委托协议,可以在其接口中声明,也可以在“分类”中声明。如果要象外界公布此类实现了某协议,那么就在接口中声明,而如果这个协议是个委托协议的话,那么通常只会在类的内部使用。所以说,这种情况一般都是在“分类”里声明的(也就是在.m文件中遵循协议):
@implementation EOCDataModel () <EOCNetworkFetcherDelegate>
@end
@implementation EOCDataModel
- (void) networkFetcher: (EOCNetworkFetcher *)fetcher didReceiveData: (NSData *) data {
/* Handle data */
}
- (void) networkFetcher: (EOCNetworkFetcher *)fetcehr didFailWithError: (NSError *)error {
/* Handle error */;
}
@end
委托协议中的方法一般都是“可选的”,因为扮演“受委托者”角色的这个对象未必关心其中的所有方法。这时我们就可以使用@optional关键字来标注其大部分或全部的方法:
@protocol EOCNetworkFetcherDelegate
@optional
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data;
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError: (NSError *)error;
@end
然而在委托对象上调用可选方法时,就必须提前使用类型信息查询方法,来判断这个委托对象能否响应相关选择子。
数据源模式
同样可以定一套接口令某个类经过该接口获得所要的数据,datasource模式。在这个模式里信息从数据源流向了类,而在委托模式里面信息则是从类流向委托者,这是二者的区别和联系:
在写代码的时候对上述还有存在优化:将委托对象是否能响应相关协议方法这一信息缓存至C语言中的“位段”数据类型了,使用这种数据类型将方法的相应能力缓存起来,在进行判断的时候只需要将缓存调出判断即可。
要点
- 委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象。
- 将委托对象应该支持的接口定义成协议,在协议中把可能需要处理的事件定义成方法。
- 当某对象需要从另外一个对象中获取数据时,可以使用委托模式。这种情况下,该模式亦称“数据源协议”
- 若有必要,可实现含有位段的结构体,将委托对象是否能响应相关协议方法这一信息缓存至其中。
24.将类的实现代码分散到便于管理的数个分类之中
类中经常容易填满各种方法,而这些方法的代码则全部堆在一个巨大的实现文件里。 有时这么做是合理的,因为即便通过重构把这个类打散,效果也不会更好。在此情况下,可 以通过Objective-C的“ 分类” 机制,把类代码按逻辑划人几个分区中,这对开发与调试都 有好处。
比如说,我们把个人信息建模为类。那么这个类就可能包含下面几个方法:
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;
- (id)initWithFirstName: (NSString *)firstName andLastName:(NSString *)lastName;
/ * Friendship methods * /
- (void) addFriend:(EOCPerson *)person;
- (void) removeFriend:(EOCPerson *)person;
- (BOOL) isFriendsWith:(EOCPerson *)person;
/ * Work methods * /
- (void) performDaysWork;
- (void) takeVacationFromWork;
/ * Play methods * /
- (void) goToTheCinema;
- (void) goToSportsGame;
@end
现在,类的实现代码按照方法分成了好几个部分。所以说,这项语言特性就叫做“分类”。本例中,类的基本要素(诸如属性与初始化方法等)都声明在“主实现”里。可是,随着分类数量增加,当前这份实现文件很快就膨胀得无法管理了,此时就可以把每个分类提取到各自的文件中去,以EOCPerson为例,可以按照其分类拆分成下列几个文件:
EOCPerson + Friendship(.h/.m)
EOCPerson + Work(.h/.m)
EOCPerson + Play(.h/.m)
//EOCPerson + Friendship.h
#import "EOCPerson.h"
@interface EOCPerson (Friendship)
- (void) addFriend:(EOCPerson *)person;
- (void) removeFriend:(EOCPerson *)person;
- (void) isFriendsWith:(EOCPerson *)person;
@end
//EOCPerson + Friendship.m
#import "EOCPerson + Friendship.h"
@implementation EOCPerson (Friendship)
- (void) addFriend: (EOCPerson *)person {
/* ... */
}
- (void) removeFriend:(EOCPerson *)person {
/* ... */
}
- (BOOL) isFriendsWith:(EOCPerson *)person {
/* ... */
}
@end
并且我们之前有说过私有方法的命名,通过特殊的前缀将私有方法指示出来,那么我们学了分类规划之后,我们还可以通过创建一个分类,这个分类其中全是私有方法,通过这种方法将这些私有方法都规划到一个类中,当然其还是的遵循之前的命名规则。
要点
- 使用分类机制把类的实现代码划分成易于管理的小块。
- 将应该视为“私有”的方法归入名叫Private的分类中,以隐藏实现细节。
25.总是为第三方类的分类名称加前缀
分类机制通常用 于向无源码的既有类中新增功能。这个特性极为强大,但在使用时也很 容易忽视其中可能产生的问题。这个问题在于:分类中的方法是直接添加在类里面的,它们 就好比这个类中的固有方法。将分类方法加人类中这一操作是在运行期系统加载分类时完成 的。运行期系统会把分类中所实现的每个方法都加人类的方法列表中。如果类中本来就有此 方法,而分类又实现了一次,那么分类中的方法会覆盖原来那一份实现代码。实际上可能会 发生很多次覆盖,比如某个分类中的方法覆盖了“ 主实现” 中的相关方法,而另外一个分类 中的方法又覆盖了这个分类中的方法。多次覆盖的结果以最后 一个分类为准。
比方说,要给NSString 添加分类,并在其中提供一些辅助方法,用于处理与HTTP URL 有关的字符串。你可能会把分类写成这样:
@interface NSString (HTTP)
//Encode a string with URL encoding
- (NSString *)urlEncodeString;
//Decode a URL encoded string
- (NSString *)urlDecodedString;
@end
现在看起来没什么问题,可是,如果还有 一个分类也往NSString 里添加方法,那会如何呢? 那个分类里可能也有个名叫urIEncodedstri ng 的方法,其代码与你所添加的大同小异,但却不能正确实现你所需的功能。 那个分类的加载时机如果晚于你所写的这个分类,那么其代码就会把你的那 一份覆盖掉,这样的话,你在代码中调用url Encodedstring 方法时, 实际执行的是那个分类里的实现代码。由 于其执行结果和你预期的值不同,所以自己所写 的那些代码也许就无法正常运行了。这种bug 很难追查,因为你可能意识不到实际执行的 urlEncodedString代码并不是自己实现的那 一份。
要解决此问题,一般的做法是:以命名空间来区别各个分类的名称与其中所定义的方法。 想在Objective-C中实现命名空问功能,只有 一个办法,就是给相关名称都加上某个共用的 前缀。与给类名加前级(参见第15条)时所应考虑的因素相似,给分类所加的前缀也要选 得怡当才行。一般来说,这个前级应该与应用程序或程序库中其他地方所用的前缀相同。 于 是,我们可以给刚才那个NSString分类加上ABC前缀:
@interface NSString (ABC_HTTP)
//Encode a string with URL encoding
- (NSString *)abc_urlEncodedString;
//Decode a URL encoded string
- (NSString *)abc_urlDecodedString;
@end
要点
- 向第三方类中添加分类时,总应给其名称加上你专用的前缀
- 向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀
26.勿在分类中声明属性
属性是封装数据的方式(参见第6 条)。尽管从技术上说,分类里也可以声明属性,但这种做法还是要尽量避免。原因在于,除了“class-continuation分类 ” ( 参见第27条 )之外 ,其 他分类都无法向类中新增实例变量,因此,它们无法把实现属性所需的实例变量合成出来。 比方说你实现过一个表示个人信息的类,在读过第24条之后,决定用分类机制将其代码分段。那么你可能会设计一个专门处理交友事务的分类,其中所有方法都与操作某人的朋友列表有关。若是不知道刚才讲的那个问题,可能就会把代表朋友列表的那项属性也放到 Friendship 分类里面去了:
#import <Foundation/Foundation.h>
@interface EOCPerson: NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
- (id) initWithFirstName:(NSString *) firstName andLastName: (NSString *) lastName;
@end
@implementation EOCPerson
//Methods
@end
@interface EOCPerson (Friendship)
@property (nonatomic, strong) NSArray *friends;
- (BOOL) isFriendsWith:(EOCPerson *)person;
@end
@implementation EOCPerson (Friendship)
//Methods
@end
编译这段代码时,编译器会给出警告信息
意思是说此分类无法合成与friends 属性相关的实例变量, 所以开发者需要在分类中为该属性实现存取方法。此时可以把存取方法声明为@dynamic, 也就是说,这些方法等到运行期再提供,编译器目前是看不见的。如果决定使用消息转发机 制(参见第12 条)在运行期拦截方法调用,并提供其实现,那么或许可以采用这种做法。
关联对象能够解决在分类中不能合成实例变量的问题。比方说,我们可以在分类中用下面这段代码实现存取方法:
#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
要点:
- 把封装数据所用的全部属性都定义在主接又里。
- 在“ class-continuation 分类” 之外的其他分类中,可以定义存取方法,但尽量不要定 义属性。
27.使用 “class-continuation 分类” 隐藏实现细节
class-continuation分类:
OC动态消息系统的工作方式决定了其不可能实现真正的私有方法或者私有实例变量。那么怎么实现私有变量和私有方法呢?这就要用到特殊的“class-continuation分类”了。
“class-continuation分类”和普通的分类不同,他必须定义在其所接续的那个类的实现文件里,并且这个类没有名字。
@interface EOCPerson ()
// Methods here
@end
这样你就可以在其中定义你的私有方法和私有变量了,这样有什么好处呢?公共接口里本来就能定义实例变量。不过,把它们定义在“class-continuation分类”或“实现块”中可以将其隐藏起来,只供本类使用。这些实例变量也并非真的私有,因为在运行期总可以调用某些方法绕过此限制,不过,从一般意义上来说,他们还是私有的。此外,由于没有声明在公共头文件里,所以将代码作为程序库的一部分来发行时,其隐藏程度更好。
“class-continuation分类”的合理用法:
“class-continuation分类”还有一种合理用法,就是将public接口中声明为“只读”的属性扩展为“可读写”,以便在类的内部设置其值。
就是说,你在外部.h文件中定义一个“只读”的属性,然后你又在“class-continuation分类”将其的“只读”属性改为“可读写”的,那么这样下来,在外部看来他就是一个“只读”的属性,但是你可以在其内部自定义的设置其值了,他在内部来说就是“可读写”的了。
这样做很有用,既能令外界无法修改对象,又能在其内部按照需要管理其数据。这样,封装在类中的数据就由实例本身来控制,而外部代码则无法修改其值。
还有一种用法:若对象所遵从的协议只应视为私有,则可在“class-continuation分类”中声明名,这样就不会泄漏我们所遵从的协议:
#import "EOCPerson.h"
#import "EOCSecretDelegate.h"
@interface EOCPerson () <EOCSecretDelegate>
@end
@implementation EOCPerson
/* ... */
@end
要点
- 通过“class-continuation分类”向类中新增实例变量。
- 如果某属性在主接口中声明为“只读”,而类的内部又要用设置方法修改此属性,要么就在“class-continuation分类”中将其扩展为“可读写”。
- 把私有方法的原型声明在“class-continuation分类”里面。
- 若想使类所遵循的协议不为人所知,则可于“class-continuation分类”中声明。
- 这一点是我觉得用处很大的一点,“class-continuation分类能够在保证私密性的前提下还能够合理的使用数据,隐藏数据。
28.通过协议提供匿名对象
若是接口背后有多个不同的实现类,而你又不想指明具体使用哪个类,那么可以考虑用这个方法——因为有时候这些类可能会变,有时候他们又无法容纳于标准的类继承体系中,因而不能以某个公共基类来统一表示。此概念通常称为“匿名对象”。
@property (nonatomic, weak) id<EOCDelegete> delegate;
这个delegate就是“匿名的”,因为当你调用这个delegate的时候你并不知道它指的是那个类,而你却又能使用它所指代类的方法,这就把那个类给隐藏起来了,匿名对象也是同样的原理。
因为你可能定义很多的类,但是我们不能将它们都继承于同一个类,并且在OC中只有id类型可以将这些类的随便一个类都返回,所以我们在使用匿名对象的时候一定是返回的id类型。比如:我们将所有数据库都具备的那些方法放到协议中,令返回的对象遵从此协议。
先定义一个协议其中包括数据库都有的方法:
@protocol EOCDatabaseConnection
- (void)connect;
- (void)disconnect;
- (BOOL)isConneceted;
- (NSArray *)performQuery:(NSString *)query;
@end
提供一个单例接口:
#import <Foundation/Foundation.h>
@protocol EOCDatabaseConnection;
@interface EOCDatabaseConnection;
+ (id)sharedInstance;
- (id<EOCDatabaseConnection>)connectionWithIdentifier: (NSString *)identifier;
@end
这样的话,处理数据库连接的类名称就不会暴露了,来自不同框架的那些类限制就都可以使用同一个方法来返回了,而不用对每个类都写一个这种协议。
要点
- 协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所实现的方法。
- 使用匿名对象来隐藏类型名称(或类名 )。
- 如果具体类型不重要,重要的是对象能够相应(定义在协议里的)特定方法,那么可使用匿名对象来表示。
总结
协议和分类这一章也是讲述了使用协议和分类需要注意的细节和很多能够在实际用途用到的方法,比如使用class-continuation分类隐藏实现细节等等