[编写高质量iOS代码的52个有效方法](四)接口与API设计(上)
参考书籍:《Effective Objective-C 2.0》 【英】 Matt Galloway
先睹为快
15.用前缀避免命名空间冲突
16.提供全能化初始方法
17.实现description方法
18.尽量使用不可变对象
目录
第15条:用前缀避免命名空间冲突
Objecti-C没有其他语言那种内置的命名空间机制,鉴于此,我们在起名时要设法避免潜在的命名冲突,否则很容易出现重名。
避免此问题的唯一办法就是变相实现命名空间:为所有名称都加上适当前缀。所选前缀可以是与公司、应用程序或者二者皆有关联之名。比方说,假设你所在的公司叫做Effective Widgets,那么就可以在所有应用程序都会用到的那部分代码中使用EWS作前缀,如果有些代码只用于名为Effective Browser的浏览器项目中,那这部分可以使用EWB作前缀。
使用Cocoa创建应用程序时一定要注意,Apple宣称其保留使用所有两字母前缀的权利,所有你自己选择用的前缀应该是三个字母的。不然可能会与框架中的类名发生冲突。
不仅是类名,应用程序中的所有名称都应加前缀,包括分类。还有一个容易引发命名冲突的地方,那就是类的实现文件中所有的纯C函数及全局变量。
// EOCSoundPlayer.m
#import "EOCSoundPlayer.h"
#import <AudioToolbox/AudioToolbox.h>
void completion(SystemSoundID ssID, void *clientData){
// code
}
@implementation EOCSoundPlayer
// code
@end
这段代码看起来完全正常,但在该类目标文件中的符号表中可以看到一个名叫_completion的符号,这就是completion函数。虽说此函数是在实现文件里定义的,并没有声明在头文件中,不过它仍算作顶级符号。如果别处又创建一个名叫completion的函数,则会发生重复符号错误。由此可见,应该总是给这种C类型函数的名字加上前缀。比如可以改名为EOCSoundPlayerCompletion。
如果用第三方库编写自己的代码,这时应该给你所用的那一份第三方库代码都加上你自己的前缀。
第16条:提供全能化初始方法
所有对象均要初始化,在初始化时,有些对象可能无须开发者向其提供额外信息。这种可为对象提供必要信息以便其能完成工作的初始化方法叫做全能初始化方法。
如果创建类实例的方法有多种,仍然需要在其中选定一个全能的初始化方法,令其他初始化方法都来调用它。NSDate就是个例子,其初始化方法如下:
- (id)init;
- (id)initWithTimeIntervalSinceNow:(NSTimeInterval)secs;
- (id)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;
- (id)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti;
- (id)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
在上面几个方法中initWithTimeIntervalSinceReferenceDate:是全能初始化方法。其余的初始化方法都要调用它。于是,只有在全能初始化方法中才会存储内部数据。
比如,要编写一个表示矩形的类。可以这样写:
// EOCRectangle.h
#import <Foundation/Foundation.h>
@interface EOCRectangle : NSObject
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;
// 全能初始化方法
- (id)initWithWidth:(float)width andHeight:(float)height;
@end
// EOCRectangle.m
#import "EOCRectangle.h"
@implementation EOCRectangle
-(id)initWithWidth:(float)width andHeight:(float)height{
if ((self = [super init])) {
_width = width;
_height = height;
}
return self;
}
@end
可是,如果用[[EOCRectangle alloc] init]方法来创建矩形,会调用EOCRectangle超类NSObject实现的init方法,调用完该方法后,全部实例变量都将设为0,所以应该使用下面两种版本中的一种来重写init方法:
// 设置默认值调用全能初始化方法
- (id)init{
return [self initWithWidth:5.0 andHeight:10.0];
}
// 抛出异常
- (id)init{
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithWidth:andHeight: instead." userInfo:nil];
}
再创建一个EOCSquare类,为EOCRectangle类的子类,专门用来表示正方形,宽度与高度必须相等。这时候需要注意的是,全能初始化方法的调用链一定要维系,如果超类的初始化方法不适用于子类,那么应该重写这个超类方法。子类的全能初始化方法与超类的名称不同时,应该重写超类的全能初始化方法。
// EOCSquare.h
#import "EOCRectangle.h"
@interface EOCSquare : EOCRectangle
// 子类的全能初始化方法
- (id)initWithDemension:(float)dimension;
@end
// EOCSquare.m
#import "EOCSquare.h"
@implementation EOCSquare
- (id)initWithDemension:(float)dimension{
return [super initWithWidth:dimension andHeight:dimension];
}
// 利用子类的全能初始化方法重写超类的全能初始化方法
- (id)initWithWidth:(float)width andHeight:(float)height{
float dimension = MAX(width, height);
return [self initWithDemension:dimension];
}
@end
不需要再在子类中重写init方法了,当用[[EOCSquare alloc] init]创建对象时,会调用超类EOCRectangle的init方法,超类的init方法已经重写,会抛出异常或者调用initWithWidth:andHeight:方法,由于子类重写了该方法,所以执行的是子类的该方法,而子类的该方法又会调用子类的全能初始化方法。
第17条:实现description方法
调试程序时,经常需要打印并查看对象信息。一种办法是编写代码把对象的全部属性都输出到日志中。不过常用的做法还是像下面这样:
NSArray *object = @[@"A string", @(123)];
NSLog(@"object = %@", object);
则会输出:
object = (
"A string",
123
)
如果在自定义类上这么做的话,输出结果会是这样:
object = <EOCPerson: 0x7fd9a1600600>
这样的信息不太有用,如果没有在自己的类里重写description方法,就会调用NSObject类所实现的默认方法。此方法定义NSObject协议里,只输出类名和对象的内存地址。想输出更多有用信息,就需要重写description方法:
#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
- (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName{
if ((self = [super init])) {
_firstName = [firstName copy];
_lastName = [lastName copy];
}
return self;
}
// 实现description方法
- (NSString*)description{
return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">",[self class], self, _firstName, _lastName];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
EOCPerson *person = [[EOCPerson alloc] initWithFirstName:@"Bob" lastName:@"Smith"];
NSLog(@"person = %@", person);
}
return 0;
}
运行结果:
2016-07-25 17:31:35.376 OCTest[41472:1864742] person = <EOCPerson: 0x1005001c0, "Bob Smith">
也可以用字典的格式来打印对象属性:
- (NSString*)description{
return [NSString stringWithFormat:@"<%@: %p, %@>",[self class], self, @{@"firstName":_firstName,@"lastName":_lastName}];
}
运行结果:
2016-07-25 17:37:16.000 OCTest[41801:1868356] person = <EOCPerson: 0x1005001a0, {
firstName = Bob;
lastName = Smith;
}>
在NSObject协议中还有一个与description方法非常类似的方法debugDescription。debugDescription方法是开发者在调试器中以控制台命令打印对象时才调用的。在NSObject类的默认实现中,此方法只是直接调用了description。
如果我们只想在description方法中描述对象的普通信息,而将更详尽的内容放在调试所用的描述信息里,此时可用下列代码实现这两个方法:
- (NSString*)description{
return [NSString stringWithFormat:@"%@ %@",_firstName, _lastName];
}
- (NSString*)debugDescription{
return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">",[self class], self, _firstName, _lastName];
}
在代码中插入断点:
int main(int argc, const char * argv[]) {
@autoreleasepool {
EOCPerson *person = [[EOCPerson alloc] initWithFirstName:@"Bob" lastName:@"Smith"];
NSLog(@"person = %@", person);
// 在本行插入断点
}
return 0;
}
当程序运行到断点时,在调试控制台输入命令。LLDB的po命令可以完成对象打印工作,这时候在控制台输入po person,运行结果如下:
2016-07-25 17:43:54.427 OCTest[42181:1872419] person = Bob Smith
(lldb) po person
<EOCPerson: 0x100101d90, "Bob Smith">
第18条:尽量使用不可变对象
默认情况下,属性是“既可读又可写的”(readwrite),这样设计出来的类都是可变的。不过一般情况下我们要建模的数据未必是需要改变的。
有时可能想修改封装在对象内部的数据,但是却不想令这些数据为外人所改动。这种情况下通常做法是在对象内部将readonly属性重新声明为readwrite。这一操作可于class-continuation分类(扩展)中完成。另外,对象里表示各种容器的属性也可以设为不可变的,通过下面的方法来使类的用户操作此属性:
// EOCPerson.h
#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;
// 提供给类的用户操作friends属性的方法
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
@end
// EOCPerson.m
#import "EOCPerson.h"
@interface EOCPerson()
// 在扩展中重新声明属性为可读写的,在对象内部可以操作属性
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
@implementation EOCPerson{
NSMutableSet *_internalFriends;
}
// 重写friends的getter方法
- (NSSet*)friends{
return [_internalFriends copy];
}
// 向friends中添加对象
- (void)addFriend:(EOCPerson *)person{
[_internalFriends addObject:person];
}
// 从friends中移除对象
- (void)removeFriend:(EOCPerson *)person{
[_internalFriends removeObject:person];
}
- (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName{
if ((self = [super init])) {
_firstName = [firstName copy];
_lastName = [lastName copy];
_internalFriends = [NSMutableSet new];
}
return self;
}
@end
如果用NSMutableSet来实现friends属性的话,可以不用addFriend:与removeFriend:方法就能直接操作此属性。但是这样做很容易出bug,采用这种做法,就等于直接从底层修改了其内部用于存放朋友对象的set。而EOCPerson毫不知情,可能会令对象内的各数据不一致。
另外需要注意的是,用扩展的方式将属性从readonly改为readwrite来实现属性仅供对象内部修改的做法也有一些漏洞。在对象外部,仍然可以通过键值编码(KVC)来修改这些属性。例如:
[person setValue:@"Seven" forKey:@"firstName"]
或者不通过setter方法,直接用类型信息查询功能查出属性所对应的实例变量在内存布局中的偏移量,以此来认为设置这个实例变量的值。
不过这两种做法都是不合规范的,不应该因为这个原因就放弃尽量编写不可变对象的做法。