第三章 接口与API设计
第15条 用前缀避免命名空间冲突
Objective—C 没有内置的命名空间(namespace)机制,所以我们在起名时要避免潜在的命名冲突。
避免此问题唯一的方法就是变相实现命名空间,为所有名称加上适当的前缀,由于Apple宣城其保留使用所有“两字母前缀”(two-letter prefix)的权利,所以要选用最少三个字母的前缀
第16条 提供 “全能初始化方法”
可为对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”
如果创建类实例的方法有很多种,就需要选定一个全能初始化方法,令其他初始化方法都来调用它。
- 全能初始化方法是初始化方法中参数最多的一个,这样才可以保证其他的初始化方法都调用自己,不会丢失参数
- 只有在这个全能初始化方法里面才能存储内部数据,这样一来,当底层数据存储机制改变时,只需要改动全能初始化方法就好。
- 当我们拥有一个全能初始化方法的时候,最好是覆写默认的init初始化方法来设置默认值
- 如果子类的全能初始化方法与超类中不同,则需要覆写超类中对应的方法,如果超类中的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常
注意 有时候需要编写多个全能初始化方法,那么每个子类的全能初始化方法都应该调用其超累的对应方法,并逐层向上,在调用了超类的初始化方法后,再执行与本类相关的方法
第17条 实现description方法
在打印自定义类的信息时,输出的信息是这样:
object = <Person: 0x7fd9a1600600>
显然平时调试的时候是为了查看类中的属性信息而不是内存地址(当然并不绝对)
此时可以覆写description方法,就可以在打印实例对象的时候得到自己想要的信息
- (NSString*)description {
return [NSString stringWithFormat:@"<%@: %p, %@ %@>", [self class], self, firstName, lastName];
}
如果我们将这些属性值放在字典里打印,则更具有可读性:
- (NSString*)description {
return [NSString stringWithFormat:@"<%@: %p, %@>",[self class],self,
@{ @"title":_title,
@"latitude":@(_latitude),
@"longitude":@(_longitude)}
];
}
打印结果
location = <EOCLocation: 0x7f98f2e01d20, {
latitude = "51.506";
longitude = 0;
title = London;
}>
第18条 尽量使用不可变对象
在设计一个类的时候,尽量把对外公布的属性在头文件内设置为只读,在实现文件内设置为读写(同一个属性在头文件中用readonly修饰,实现文件中用readwrite修饰)这样一来,在外部就只能读取该数据,而不能修改它,是的这个类的实例所持有的数据更加安全
对于集合类的对象,更应该考虑是否可以将其设置为可变的,如果在公开部分只能设置其为只读属性,那么就在非公开部分存储一个可变型,这样一来当在外部获取这个属性时,获取的只是内部的可变型的一个不可变版本
@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 andLastName:(NSString*)lastName;
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
@end
@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
@implementation EOCPerson {
NSMutableSet *_internalFriends; //实现文件里的可变集合
}
- (NSSet*)friends {
return [_internalFriends copy]; //get方法返回的永远是可变set的不可变型
}
- (void)addFriend:(EOCPerson*)person {
[_internalFriends addObject:person]; //在外部增加集合元素的操作
//do something when add element
}
- (void)removeFriend:(EOCPerson*)person {
[_internalFriends removeObject:person]; //在外部移除元素的操作
//do something when remove element
}
- (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName {
if ((self = [super init])) {
_firstName = firstName;
_lastName = lastName;
_internalFriends = [NSMutableSet new];
}
return self;
}
在公共接口设置不可变set和将增删的代码放在公共接口中并不矛盾,因为如果将set设置为可变类型的话,在外部就可以直接修改其底层的数据,不会伴随其他操作,然而有时候我们需要在set数据源改动的时候执行其他的相关操作,此时直接修改外部的set是达不到这种效果的,所以我们要尽量提供给外界定制的增删方法,不要让外部直接针对set属性增删
第19条 使用清晰而协调的命名方式
Objective-C 的命名方式相较于其他语言比较繁琐,但却非常清晰,读起来像日常语言里的句子
注意
- 命名时要村从标准的Objective-C命名规范
- 方法名中不要使用缩略后的类型名称
第20条 为私有方法名加前缀
针对内部使用的方法,最好是为这种方法的名称加上某些前缀,这样容易把公共方法和私有方法区别开,方便调试
#import <Foundation/Foundation.h>
@interface EOCObject : NSObject
- (void)publicMethod;
@end
@implementation EOCObject
- (void)publicMethod {
/* ... */
}
- (void)p_privateMethod {
/* ... */
}
@end
注意不要用下划线来区分私有方法和公共方法,因为这有可能会和苹果公司的API重复
第21条 理解Objective—C 错误模型
由于Objective—C的内存管理特性 NSError 比 Exception机制更适合描述错误类型
只有在发生了可使整个应用程序崩溃的严重错误时才应该使用异常
NSError对象封装了三种信息
- Error domain (错误范围,字符串类型)
- Error code (错误码,int类型)
- User info (用户信息,字典类型)
使用时:
- 在错误不是很严重的情况下,可以指派代理方法来处理错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
- 另一种常见用法是,经过“输出参数”返回给调用者
- (BOOL)doSomething:(NSError**)error;
NSError *error = nil;
BOOL ret = [object doSomething:&error];
if (error) {
// There was an error
}
可以自定义NSError
- 错误范围用全局常量字符串来定义
- 错误码用枚举来定义
// EOCErrors.h
extern NSString *const EOCErrorDomain;
//定义错误码
typedef NS_ENUM(NSUInteger, EOCError) {
EOCErrorUnknown = –1,
EOCErrorInternalInconsistency = 100,
EOCErrorGeneralFault = 105,
EOCErrorBadInput = 500,
};
// EOCErrors.m
NSString *const EOCErrorDomain = @"EOCErrorDomain"; //定义错误范围
第22条 理解NSCopying协议
一个类如果想要实现拷贝操作,就需要实现NSCopying协议,实现这个协议后这个类的实例对象就可以用copy方法实现拷贝
此协议只有一个方法
- (id)copyWithZone:(NSZone*)zone
注意:NSZone代表不同的“区”,以前的开发分为很多分区,对象会创建到相应的区里,现在的程序只有一个“默认区”,所以不用担心zone的参数
让一个自定义类实现copy功能
- (id)copyWithZone:(NSZone*)zone {
EOCPerson *copy = [[[self class] allocWithZone:zone] initWithFirstName:_firstName andLastName:_lastName];
copy->_friends = [_friends mutableCopy];
return copy;
}
如果自定义的对象分为不可变和可变版本,那就要同时实现NSCopying、NSMutableCopying两个协议
深拷贝与浅拷贝的区别:
- 浅拷贝:之拷贝容器对象本身,不复制其中的数据(Foundation框架中的多有colection类在默认情况下都是执行的浅拷贝)
-
深拷贝:在靠背对象自身时,将其底层数据也一并复制
image
深拷贝的执行方式需要由类自己确定,系统没有专门定义
例如
- (id)deepCopy {
EOCPerson *copy = [[[self class] alloc] initWithFirstName:_firstName andLastName:_lastName];
copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES];
return copy;
}
第四章 协议与分类
第23条 通过委托与数据源协议进行对象间通信
“委托模式” 用来实现对象之间的相互通信:定义一套接口,某对象若想接受另一个对象的委托,则需要遵循此接口,成为“委托对象”,另一个对象则可以给委托对象回传信息,也可以在发生相关事件时通知委托对象,此模式可将数据与业务逻辑解耦。
在原对象中声明的委托对象属性要使用weak修饰,因为委托对象一般会持有原对象,如果使用strong修饰 会引入“保留环”。
在委托模式中,常规的方式是信息流从类流向委托对象,但也可以让信息从数据源流向类,这种模式叫做“数据源模式”
image例如TableView的Delegate和DataSource,Delegate属于信息从TableView流向它的代理对象,DataSource属于信息从代理对象流向TableView,这两个信息的传递方向是相反的
第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
分类之后
#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;
@end
@interface EOCPerson (Friendship)
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(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)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条 总是为第三方分类的分类名称添加前缀
分类中的方法是直接添加到本类里边的,这一操作是在运行期系统加载分类时完成的,系统会把分类中的方法加入到类的方法列表中,如果这个方法本来就存在,那么它就会被分类中的同样的方法覆盖,多个分类中有相同的方法的话,系统会议最后一个加载的分类中的方法为准来执行。
解决这个问题一般的方法是用命名空间将各个分类中相同的方法区别开来,而在Objective-C中,想实现命名空间只能是在方法名上加上自定义的前缀。
例如:
@interface NSString (ABC_HTTP)
// Encode a string with URL encoding
- (NSString*)abc_urlEncodedString;
// Decode a URL encoded string
- (NSString*)abc_urlDecodedString;
@end
方法被覆盖虽然不会发生异常,但是编译器会发出警告warning:duplicate definition of category 'HTTP' on interface 'NSString'
第26条 勿在分类中声明属性
分类机制目标在于扩展类的功能,而非封装数据
分类并不会改变原有类的内存分布的情况,它是在运行期间决定的,此时内存的分布已经确定,若此时再添加实例会改变内存的分布情况,这对编译性语言是灾难,是不允许的。反观扩展(extension),作用是为一个已知的类添加一些私有的信息,必须有这个类的源码,才能扩展,它是在编译器生效的,所以能直接为类添加属性或者实例变量。
虽然通过关联对象原理 可以实现 为分类添加属性的效果,但不建议使用,因为此方法并不一定健壮,有可能出现内存问题,建议把属性都定义在主接口中
第27条 使用 “class-continuation分类” 隐藏实现细节
当类中有某些实例变量或方法无须对外公布的时候,可以将这些方法或实例变量房间 class-continuation分类 中,它是唯一可以声明实例变量的分类,(这个分类必须定义在本类的实现文件中)
class-continuation分类的特性
- 在分类总增加实例变量
- 将公共接口中的只读属性设置为读写属性
- 遵循协议,使外界不为人知
第28条 通过协议提供匿名对象
用协议来提供匿名对象,目的在于说明它仅表示“遵从某个协议的对象”,而不是属于某个类的对象
id<protocol>
使用场景
1. 匿名对象作为属性
@property (nonatomic, weak) id <EOCDelegate> delegate;
任何遵从EOCDelegate
协议的类 都可以成为这个类的delegate
2. 匿名对象作为方法参数
- (void)setObject:(id)object forKey:(id<NSCopying>)key;
key参数只要遵从了NSCopying
协议,就可以作为NSDictionary的键
部分图片、代码、笔记源自J_Knight_的博客
网友评论