[编写高质量iOS代码的52个有效方法](六)协议与分类
参考书籍:《Effective Objective-C 2.0》 【英】 Matt Galloway
先睹为快
23.通过委托与数据源协议进行对象间通信
24.将类的实现代码分散到便于管理的数个分类之中
25.总是为第三方类的分类名称加前缀
26.勿在分类中声明属性
27.使用class-continuation分类隐藏实现细节
28.通过协议提供匿名对象
目录
第23条:通过委托与数据源协议进行对象间通信
Objective-C广泛使用一种名叫委托模式的编程设计模式来实现对象间的通信,该模式的主旨是:定义一套接口,某对象若想接受另一个对象的委托,则需遵从此接口,以便成为其委托对象(delegate)。而这另一个对象则可以给其委托对象回传一些信息,也可以在发生相关事件时通知委托对象。
比如,用户界面里有个显示一些列数据所用的视图,那么,此视图只应包含显示数据所需要的逻辑代码,而不应决定要显示何种数据以及数据之间如何交互等问题。视图对象的属性中,可以包含负责数据与事件处理的对象。这两种对象分别称为数据源(data source)和委托(delegate)。
下面是一个使用委托的示例。有一个从网上获取数据的类,含有一个委托对象,在获取完数据之后,它会回调这个委托对象。
// 定义协议
@protocol EOCNetworkFetcherDelegate <NSObject>
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didReceiveData:(NSData*)data;
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didFailWithError:(NSError*)error;
@end
// 获取网络数据的类接口
@interface EOCNetworkFetcher : NSObject
// 用属性来存放其委托对象,注意属性必须定义为weak,否则会形成保留环
@property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate;
@end
// 委托对象类实现文件
// 扩展中声明遵循EOCNetworkFetcherDelegate协议
@interface EOCDataModel()<EOCNetworkFetcherDelegate>
@end
@implementation EOCDataModel
// 实现协议中的方法
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didReceiveData:(NSData*)data{
// 处理数据
}
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didFailWithError:(NSError*)error{
// 处理错误
}
@end
委托协议中的方法一般都是可选的,因为受委托的这个对象未必关心其中的所有方法。所以委托协议中进场用@optional关键字来标注大部分或全部的方法:
@protocol EOCNetworkFetcherDelegate <NSObject>
@optional
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didReceiveData:(NSData*)data;
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didFailWithError:(NSError*)error;
@end
如果要在委托对象上调用可选方法,那么必须提前使用类型信息查询方法判断这个委托对象是否能相应相关选择器。
NSData *data = /* 网络获取的数据 */;
if([_delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)]){
[_delegate networkFetcher:self didReceiveData:data];
}
但是如果每次都检查委托对象是否能相应此选择器显得有些多余了,可以使用位段数据类型将方法相应能力缓存为结构体中的标志。
// 在扩展中定义结构体
@interface EOCNetworkFetcher(){
struct {
unsigned int didReceiveData : 1;
unsigned int didFailWithError : 1;
}_delegateFlags;
}
@end
@implementation EOCNetworkFetcher
- (void)setDelegate:(id<EOCNetworkFetcherDelegate>)delegate{
_delegate = delegate;
// 缓存委托对象相应方法能力
_delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
_delegateFlags.didFailWithError = [delegate respondsToSelector:@selector(networkFetcher:didFailWithError:)];
}
@end
这样每次调用delegate相关方法之前就只需要直接查询标志:
if(_delegateFlags.didReceiveData){
[_delegate networkFetcher:self didReceiveData:data];
}
第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) NSSet *friends;
- (id)initWithFirstName:(NSString*)firstName lastName:(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+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)
// 实现接口中定义的三个方法
@end
// 类似地编写:EOCPerson+Work(.h/.m),EOCPerson+Play(.h/.m)
使用分类不仅让代码功能更明确,还有助于调试。对于某个分类中的所有方法来说,分类名称都会出现在其符号中。根据调试器的回溯信息,可以定缺定位到类中方法所属的功能区。
在编写准备分享给其他开发者使用的程序库时,可以考虑创建Private分类,将应该视为私有的方法归入名叫Private的分类中,以隐藏细节。
第25条:总是为第三方类的分类名称加前缀
分类机制通常用于向无源码的既有类中新增功能。这个特性极为强大,但在使用时也很容易忽视其中可能产生的问题:分类中的方法是直接添加到类里面的,它们就好比这个类中的固有方法,如果类中本来就有此方法,而分类又实现了一次,那么分类中的方法会覆盖原来的那一份代码,实际上甚至有可能发生多次覆盖。
例如给NSString添加分类,用于处理与HTTP URL有关的字符串。
@interface NSString(HTTP)
- (NSString*)urlEncodedString;
- (NSString*)urlDecodedString;
@end
现在看来没什么问题,但是如果又有另一个分类野望NSString中添加名叫urlEncodedString的方法,那么就会出现覆盖,导致其中一个方法的代码无法正常运行。
要解决此问题,一般做法就是通过加前缀来实现命名空间对各个分类的名称以及其中方法进行区别:
@interface NSString(ABC_HTTP)
- (NSString*)abc_urlEncodedString;
- (NSString*)abc_urlDecodedString;
@end
第26条:勿在分类中声明属性
属性是封装数据的方式。尽管从技术上来说,分类里也可以声明属性,但这种做法还是要尽量避免。原因在于,除了class-continuation分类(扩展)之外,其它分类都无法向类中新增实例变量,它们无法把实现属性所需的实例变量合成出来。
例如以下代码:
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName;
@end
@implementation EOCPerson
// 实现方法
@end
// 在分类中声明了属性
@interface EOCPerson (Friendship)
@property (nonatomic, strong, readonly) NSSet *friends;
- (BOOL)isFriendWith:(EOCPerson*)person;
@end
@implementation EOCPerson (Friendship)
// 实现方法
@end
编译这段代码时,编译器会发出警告:此分类无法合成与friends属性相关的实例变量,所以开发者需要在分类中为该属性实现存取方法。
此时可以把存取方法声明为@dynamic,再在运行期用关联对象解决:
#import "<objc/runtime.h>"
static const char *kFriendsPropertyKey = "kFriendsPropertyKey";
@implementation EOCPerson (Friendship)
@dynamic friends;
- (NSSet*)friends{
return objc_getAssociatedObject(self,kFriendsPropertyKey);
}
- (void)setFriends:(NSSet*)friends{
objc_setAssociatedObject(self,kFriendsPropertyKey,friends,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
这样做可行,但不太理想。要把相似的代码写很多遍,而且在内存管理问题上容易出错,所以最好的办法还是把所有的属性都声明在主接口中。分类的目标在于扩展类的功能,而非封装数据。
第27条:使用class-continuation分类隐藏实现细节
类中经常会包含一些无须对外公布的方法及实例变量。这时候就可以用到扩展(class-continuation分类),扩展和普通的分类不同,它必须定义在其所接续的那个类的实现文件里。其重要之处在于,这是唯一能声明实例变量的分类,而且此分类的方法都定义在类的主实现文件里。
在公共接口里定义实例变量,即使标注为private,还是会泄漏细节,比如有个绝密的类,不想给他人知道,如果定义在公共接口中:
#import <Foundation/Foundation.h>
@class EOCSuperSecretClass;
@interface EOCClass : NSObject{
@private
EOCSuperSecretClass *_secretInstance;
}
@end
那么,信息就泄漏了,别人就知道有个名叫EOCSuperSecretClass的类了。如果把实例变量声明为id,在类内部使用此实例时又无法获得编译器的帮助。
这个问题可以用扩展来解决:
#import <Foundation/Foundation.h>
@interface EOCClass : NSObject{
@end
#import "EOCClass.h"
#import "EOCSuperSecretClass.h"
@interface EOCClass(){
EOCSuperSecretClass *_secretInstance;
}
@end
@implementation EOCClass
// code
@end
由于没有声明在公共头文件里,这样隐藏程度更好。
扩展还可以用于将公共接口中声明为只读的属性扩展为可读写的,以便在类的内部设置其值:
// EOCPerson.h
@interface EOCPerson : NSObject
// 声明只读属性
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName;
@end
// EOCPerson.m
@interface EOCPerson ()
// 将只读属性扩展为可读写
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
扩展也可以用来声明只在类实现代码中才会使用的私有方法:
@interface EOCPerson ()
- (void)p_privateMethod;
@end
扩展的最后一种用法是:若对象遵从了某个秘密协议,不想在公共接口中泄漏这一信息,可以在扩展中声明
#import "EOCPerson.h"
#import "EOCSecretDelegate.h"
@interface EOCPerson ()<EOCSecretDelegate>
@end
@implementation EOCPerson
// code
@end
第28条:通过协议提供匿名对象
协议定义了一系列方法,遵从此协议的对象应该实现它们。于是我们可以用协议把自己所写的API之中的实现细节隐藏起来,将返回的对象设计为遵从此协议的纯id类型。因为接口背后可能有多个不同的实现类,这些类可能会变,有时候它们又无法容纳于标准的类继承体系中,因而不能以某个公共基类来统一表示。
此概念经常称为匿名对象,在23条委托与数据源对象中,就曾用到这种匿名对象。
@property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate;
由于该属性类型是id,所以实际上任何类的对象都能充当这一属性,即便该类不继承自NSObject,只要遵循EOCNetworkFetcherDelegate协议就行。
NSDictionary也能实际说明这一概念,在可变版本字典中,设置键值对所用的方法是:
- (void)setObject:(id)object forkey:(id<NSCopying>)key
表示键的那个参数类型为id,字典对象只需要确定它可以给key对象发送拷贝信息就行了,而不关心此实例所属的具体类。
处理数据库连接的程序库也用这个思路,以匿名对象来表示从另一个库中返回的对象。
@protocol EOCDatabaseConnection
- (void)connect;
- (void)disconnect;
- (BOOL)isConnected;
- (NSArray*)performQuery:(NSString*)query;
@end
#import <Foundation/Foundation.h>
@protocol EOCDatabaseConnection
@interface EOCDatabaseManager : NSObject
+ (id)sharedInstance;
- (id<EOCDatabaseConnection>)connectionWithIdentifier:(NSString*)identifier;
@end
这样的话,处理数据库连接所用的类的名称就不会泄露了,有可能来自不同框架(如MySQL、PostgreSQL)的那些类现在均可以经由同一个方法来返回了,使用此API的人仅仅要求所返回的对象能用来连接、断开并查询数据库即可。