第24条:将类的实现代码分散到便于管理的数个分类之中
本条要点:(作者总结)
- 使用分类机制把类的实现代码划分成易于管理的小块
- 将应该视为 “私有” 的方法归入名叫 Private 的分类中,以隐藏实现细节
类中经常容易填满各种方法,而这些方法的代码则全部堆在一个巨大的实现文件里。有时这么做是合理的,因为即便通过重构把这个类打散,效果也不会更好。在此情况下,可以通过 Objective-C 的 “分类”机制,把代码按逻辑划入几个分区中,这对开发与调试都有好处。
比如说,我们把个人信息建模为类。那么这个类就可能包含下面几个方法:
1 #import <Foundation/Foundation.h> 2 3 @interface EOCPerson : NSObject 4 5 @property (nonatomic, copy, readonly) NSString *firstName; 6 @property (nonatomic, copy, readonly) NSString *lastName; 7 @property (nonatomic, strong, readonly) NSArray *friends; 8 9 - (instancetype)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName; 10 11 /*Friendship methods*/ 12 - (void)addFriend:(EOCPerson *)person; 13 - (void)removeFriend:(EOCPerson *)person; 14 - (BOOL)isFriendsWith:(EOCPerson *)person; 15 16 /*Work methods*/ 17 - (void)performDayWork; 18 - (void)takeVacationFromWork; 19 20 /*Play methods*/ 21 - (void)goToTheCinema; 22 - (void)goToSportsGame; 23 24 @end
在实现该类时,所有方法的代码可能会写在一个大文件里。如果还向类中继续添加方法的话,那么源代码文件就会越来越大,变得难于管理。所以说,应该把这样的类分成几个不同的部分。例如,可以用 “分类” 机制把刚才的类改成下面这样:
1 #import <Foundation/Foundation.h> 2 3 @interface EOCPerson : NSObject 4 5 @property (nonatomic, copy, readonly) NSString *firstName; 6 @property (nonatomic, copy, readonly) NSString *lastName; 7 @property (nonatomic, strong, readonly) NSArray *friends; 8 9 - (instancetype)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName; 10 11 @end 12 13 @interface EOCPerson (Friendship) 14 /*Friendship methods*/ 15 - (void)addFriend:(EOCPerson *)person; 16 - (void)removeFriend:(EOCPerson *)person; 17 - (BOOL)isFriendsWith:(EOCPerson *)person; 18 @end 19 20 @interface EOCPerson (Work) 21 /*Work methods*/ 22 - (void)performDayWork; 23 - (void)takeVacationFromWork; 24 @end 25 26 @interface EOCPerson (Play) 27 /*Play methods*/ 28 - (void)goToTheCinema; 29 - (void)goToSportsGame; 30 @end
现在,类的实现代码按照方法分成了好几个部分。所以说,这项语言特性当然就叫做 “分类”啦。在本例中,类的基本要素(诸如属性与初始化方法等)都声明在 “主实现”(main implementation)里。执行不同类型的操作所用的另外几套方法则归入各个分类中。
使用分类机制之后,依然可以把整个类都定义在一个接口文件中,并将代码写在一个实现文件里。可是,随着分类数量增加,当前这份实现文件很快就膨胀得无法管理了。此时可以把每个分类提取到各自的文件中去。以 EOCPerson 为例,可以按照其分类拆分成下列几个文件:
- EOCPerson+Friendship(.h/.m)
- EOCPerson+Work(.h/.m)
- EOCPerson+Play(.h/.m)
比方说,与交友功能相关的那个分类可以这样写:
1 #import "EOCPerson.h" 2 3 @interface EOCPerson (Friendship) 4 5 - (void)addFriend:(EOCPerson *)person; 6 - (void)removeFriend:(EOCPerson *)person; 7 - (BOOL)isFriendsWith:(EOCPerson *)person; 8 9 @end
1 #import "EOCPerson+Friendship.h" 2 3 @implementation EOCPerson (Friendship) 4 5 - (void)addFriend:(EOCPerson *)person { 6 /*...*/ 7 } 8 9 - (void)removeFriend:(EOCPerson *)person { 10 /*...*/ 11 } 12 13 - (BOOL)isFriendsWith:(EOCPerson *)person { 14 /*...*/ 15 return NO; 16 } 17 18 @end
通过分类机制,可以把类代码分成很多个易于管理的小块,以便单独检视。使用分类机制之后,如果想用分类中的方法,那么要记得在引入 EOCPerson.h 时一并引入分类的头文件。虽然稍微有点麻烦,不过分类仍然是一种管理代码的好办法。
即使类本身不是太大,我们也可以使用分类机制将其切割成几块,把相应代码归入不同的 “功能区”(functional area)中。Cocoa 中的 NSURLRequest 类及其可变版本 NSMutableURLRequest 类是这么做的。这个类用于执行从 URL 中获取数据的请求,而且通常使用 HTTP 协议从因特网中的某个服务器上获取,不过,由于该类设计得较为通用,所以也可以使用其他协议。与标准的URL 请求相比,执行 HTTP 请求时还需要另外一些信息,例如 “HTTP 方法”(这里的 “方法” 是 “动作”的意思,其含义与编程语言中用以称呼函数的 “方法” 一词不同)(HTTP method,GET、POST等)或 HTTP 头(HTTP header)。
然而却不便从 NSURLRequest 中继承子类以实现 HTTP 协议的特殊需求,因为本类包裹了一套操作 CFURLRequest 数据结构所需的 C 函数,所有 “HTTP 方法” 都包含在这个结构里。于是,为了扩展 NSURLRequest 类,把与 HTTP 有关的方法归入名为 NSHTTPURLRequest 的分类中,而把与可变版本有关的方法归入名为 NSMutableHTTPURLRequest 的分类中。这样,所有底层 CFURLRequest 函数就都封装在同一个 Objective-C 类里了,而在这个类里,与 HTTP 有关的方法却又要单独放在一起,因为若是不这么做的话,该类的使用者就会有疑问:为什么能在使用 FTP 协议的 request 对象上设置 “HTTP 方法” 呢?
之所以要将代码打散到分类中还有个原因,就是便于调试:对于某个分类中的所有方法来说,分类名称都会出现在其符号中。例如,“addFriend:” 方法的 “符号名”(symbol name)如下:
1 - [EOCPerson(Friendship) addFriend:]
在调试器的回溯信息中,会看到类似下面这样的内容:
1 frame #2: 0x00001c50 Test '-[EOCPerson(Friendship) addFriend:] 2 + 32 at main.m: 46
根据回溯信息中的分类名称,很容易就能精确定位到类中的方法所属的功能区,这对于某些应该视为私有的方法来说更是极为有用。可以创建名为 Private 的分类,把这种方法全都放在里面。这个分类里的方法一般只会在类或框架内部使用,而无须对外公布。这样一来,类的使用者有时可能会在回溯信息时发现 private 一词,从而知道不应该直接调用此方法了。这可算作一种编写 “自我描述式代码”(self-documenting code)的办法。
在编写准备分享给其他开发者使用的程序库时,可以考虑创建 Private 分类。经常会遇到这样一些方法:它们不是公共 API 的一部分,然而却非常适合在程序库之内使用。此时应该创建 Private 分类,如果程序库中的某个地方要用到这些方法,那就引入此分类的头文件。而分类的头文件并不随程序库一并公开,于是该库的使用者也就不知道库里还有这些私方法了。
END