Chapter 3 接口与 API 设计
-
Tips 15 使用前缀避免明明空间冲突
- Objective-C 没有命名空间,所以我们在起名时要设法避免命名冲突
- 避免命名冲突的方法就是使用前缀
- 应用中的所有名称都需要加前缀(包括实现文件中的全局变量和纯 C 函数)
-
Tips 16 提供“全能(designated)初始化方法”
- 一个会被所有初始化方法调用到的初始化方法
- 当底层数据存储机制变化时,只需要修改这个方法就可以了,不需要改动其他初始化方法
- 如果超类的全能初始化方法不适用于子类,或是与超类不同,那么需要覆盖这个超类方法
- 子类的全能初始化方法都应该调用超类的对应方法,逐级向上
-
Tips 17 实现 description 方法
- 在数组字典等集合对象打印时,都会调用对象的
description
方法,方便调试 - 系统默认的
description
方法对于自定义的对象并没有输出较为有用的内容,所以可以实现这个方法方便我们显示对象 - 在调试时会调用
debugDescription
方法(也就是在调试时 lldb 中输入 po 时调用的将会是debugDescription
),所以实现他可以帮助我们调试时获得更多的信息 - 可以使用
NSDictionary
来实现description
方法,这样显示和输出都会比较方便,例如:// Header File // 这里我略微修改了下原书中的示例代码 @interface EOCLocation : NSObject @property (nonatomic, copy) NSString *title; @property (nonatomic) CGFloat latitude; @property (nonatomic) CGFloat longitude; @end // 我们要是可以使用 NSLog(@"%@", eoc_location) 直接输出这个对象的经纬度(也就是所有属性)就好了,那么可以参考下面的写法实现 description 方法 @implementation EOCLocation - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p, %@>", [self class], self, @{ @"title": self.title, @"latitude": @(self.latitude), @"longitude": @(self.longitude), }]; } @end
- 在数组字典等集合对象打印时,都会调用对象的
-
Tips 18 尽量使用不可变对象
- 减少 side effect,在使用了一段时间的 RAC 和学习函数式思想后,一定程度上理解了不可变对象的好处
- 具体开发实践中,应尽量把对外公布的属性设为只读,并且有必要时才对外公布,否则使用私有属性
- 对于只读属性,可以不用指定内存管理语义(也就是 strong,weak,copy)
- 对外只读的属性可以在对象内部,也就是类扩展(Class-Extension 也叫 Class-Continuation)中重新声明为可读写的
- 可以使用 GCD 来设置读写操作为同步操作
- 就算属性设置为只读,在外部仍可以使用 KVC 来访问这些属性,例如:
[object setValue:@"value" forKey:@"propertyName"]
- 集合属性(Array,Set,Dictionary)可以提供只读属性供外界使用(内部保存可变类型的变量,返回该变量的不可变拷贝),并提供操相应的操作方法,例如下面例子中,使用
-addFriend:
和-removeFriend:
方法来实现对friends
集合的操作,这样保证了添加或删除盆友的操作对象是知情的。对于直接修改friends
集合的操作对象是不知情的,这样可能会导致对象内各数据的不一致。@interface EOCPerson : NSObject @property (nonatomic, strong, readonly) NSSet *friends; @end @implementation EOCPerson { NSMutableSet *_internalFriends; } - (NSSet *)friends { return [_internalFriends copy]; } - (void)addFriend:(EOCPerson *)person { [_internalFriends addObject:person]; } - (void)removeFriend:(EOCPerson *)person { [_internalFriends removeObject:person]; } @end
- 不要在返回的对象上查询其是否是可变对象并对其进行操作,同上条这样对对象集合属性的直接修改,容易产生 bug
-
Tips 19 使用清晰而协调的命名方式
- 方法名的风格要保证与自己的代码或是需要集成的框架一致,也就是上下文需要一致,这点最重要放第一
- 起名遵循 Objective-C 的命名规范,这样的接口名字一定程度上提示了接口的作用
- 方法名言简意赅,从左到右读起来最好像一个日常用于中的句子
- 方法名里不要使用缩略后的类型名称
- Objective-C 的方法名相较其他语言要长一些,但是可以更好地表达方法的作用,以及各个参数的意义,比如:
Rectangle *recgangle = new Rectangle(5.0f, 10.0f); // 不如下面的命名方式 Rectangle *recgangle = [Rectangle initWithSize:(float)width :(float)height]; // 不如下面的命名方式 Rectangle *recgangle = [Rectangle initWithWidth:(float)width andHeight:(float)height];
-
Tips 20 为私有方法名加前缀
- 因为在 Objective-C 中没有私有方法,所有对象都可以响应任意消息,并且可以通过 runtime 获取对象可以相应的消息,所以我们使用特定的命名来区分私有方法
- 在使用 Category 或继承系统中或第三方库中的类的时候,可以防止命名冲突
- C 语言中使用
_
下划线作为系统内部函数的开头所以我们不能使用_
作为私有方法的前缀(苹果的官方库也使用_
) - 原书作者建议使用
p_
来作为私有方法的前缀,个人建议使用开发中项目使用的前缀小写来作为类前缀,比如上文的EOCPerson
中添加私有方法可以使用eco_privateMethodName:
,这样的前缀在第三方类库中出现重复的概率比较小
-
Tips 21 理解 Objective-C 错误模型
- ARC 在默认情况下并不是异常安全的,也就是抛出异常的时候,在作用域末尾应该释放的对象将不会被释放
- 可以使用
-fobjc-arc-exceptions
来告诉编译器需要生成异常安全的代码,但是这样会引入一些额外代码,并且在不抛出异常时也会执行这部分代码 - 就算不使用 ARC 使用异常也很容易写出内存泄漏的代码,因为需要在抛出异常前清理所有申请的资源,所以现在我们只在非常罕见(严重错误,比如:抽象类中的方法没有实现)的情况下抛出异常,抛出之后不需要考虑回复的问题,并且退出应用,这样就不用编写复杂的异常安全代码
- 对于不严重的错误,我们通过返回 nil/0 或是使用
NSError
来处理,NSError
中包含了错误处理所需的各种信息,我们自己的错误需要规划和设置好对应的 Error Domain,Error Code - 一般通过 delegate 来传递错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
或是输入参数返回错误- (BOOL)doSomething:(NSError **)error
-
Tips 22 理解
NSCopying
协议- 实现
NSCopying
接口可以让类实现拷贝(copy
)方法,- (id)copyWithZone:(NSZone *)zone
中的zone
是以前开发时使用的内存区参数,目前已经不使用了,可以不用考虑他 - 实现
NSMutableCopying
协议支持可变拷贝(mutableCopy
)方法 - 对象拷贝时需要决定是深拷贝还是浅拷贝,一般情况下用浅拷贝
- 绝大多数情况下
NSCopying
实现的都是浅拷贝,所以如果使用深拷贝,建议创建一个单独的方法来完成
- 实现
Chapter 4 协议(Protocol)和分类(Category)
-
Tips 23 使用委托(delegate)和数据源(data source)协议进行对象间通信
- 委托模式(delegate pattern):对象把应对某个行为的责任委托给了另一个类
- 类似我们经常使用的
UITableView
,UITableViewDelegate
和UITableViewDataSource
分别定义了如何处理事件的接口和如何提供数据的接口,实现这两个接口为UITableView
提供交互逻辑和显示数据,UITableView
本身只负责显示获取到的数据 - 委托模式同样适用于异步事件,比如网络请求完成后,回调委托对象将结果传递回去,实现事件的异步处理
- 使用委托对象的对象中的委托对象属性需要设置为 weak,防止循环引用
- 使用委托中的方法时,使用
respondsToSelector:
先查询委托对象是否实现了该方法,特别是在协议中使用@option
关键字标注的可选方法 - 委托中的方法名要清晰明确,需要说明事件的来源,当前的事件,以及为什么委托对象需要获取这个事件,所有委托方法都需要将发起委托的对象发送到委托对象(作为第一个参数),让委托对象判断事件来源
- 针对需要进行多次调用的委托对象(例如网络加载时下载进度),可以通过结构体等方法,在设置委托对象的时候,一次检查需要响应的方法并记录,之后在使用的时候,直接通过记录结果来判断是否实现了某个方法,不用每次都使用
respondsToSelector:
方法来查询是否实现,例:
@interface EOCNetworkFetcher() { struct { unsigned int didReceiveData : 1; unsigned int didFailWithError : 1; unsigned int didUpdateProgressTo : 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:)]; _delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)]; } @end // 在需要调用 delegate 方法的时候 if (_delegateFlags.didUpdateProgressTo) { [_delegate networkFetcher:self didUpdateProgressTo:currentProgress]; }
-
Tips 24 将类的实现代码分散到便于管理的多个 Category 中
- 在开发的过程中,类的代码只会越来越大,那么我们可以通过分类机制将类的代码打散,根据业务分散到不同的分类中
- 应该把私有方法放到叫(Private)的分类中,隐藏实现细节
-
Tips 25 总是为第三方分类的分类名称加前缀
- 如果分类中出现同名方法,容易出现奇怪的 bug,所以在为其他类添加分类的时候,分类名称和分类中的方法需要添加你自己使用的前缀
-
Tips 26 勿在分类中声明属性
- 分类中可以定义方法(包括 getter 和 setter),但是不要定义属性,因为在分类中定义的属性不会生成实例变量
- 虽然有
objc_setAssociatedObject
魔法可以用,但是这容易导致内存管理问题,因为无法使用属性记录内存管理语义,但是建议一般情况下不使用 - 分类的主要作用是扩张类的功能,而不是封装数据
-
Tips 27 使用 Class-Continuation 分类,隐藏实现细节
- Class-Continuation 分类必须定义在该类的实现文件中,并且可以声明实例变量,并且建议仅以此种方式增加实例变量
- 头文件中声明为只读的属性,可以在实现文件中的 Class-Continuation 分类中扩展为可读写
- 私有方法原型,和私有属性,都可以放到 Class-Continuation 分类中
- 在 Class-Continuation 分类中可以声明实现的接口,并且外部不会知道
- 可以通过私有属性很好的封装 C++/Objective-C++ 的代码,提供 Objective-C 的接口给其他代码使用
-
Tips 28 通过协议提供匿名对象
- 使用类似
@property(nonatomic, weak) id<ProtocolName> delegate;
提供匿名类型对象作为 delegate,可以隐藏类名 - 对于类型不重要,只需要提供可向应方法的对象,可以使用匿名对象,隐藏实现细节
- 使用类似
对于 Chapter 1 的补充
第一章第四条中,多用类型常量,少用 #define
预处理指令中,建议大家使用类型常量而不是 #define
来定义常量,这里增加一个补充内容,swift 中,我们可以使用 struct
中的静态变量来声明常量,这样带来的一个好处是使用和分类管理非常方便
Xcode 8.0 带的 clang 4.0 后开始支持类常量,也就是定义属性的时候,可以加入 class
来修饰属性,这样这个属性是属于类的,于是乎,我们可以这样使用常量了
NSString *notificationName = XXXConstant.notificationNames.XXXUserDidLoginNotificationName;
看上去比类型常量长一些,不过似乎还算比较好看
定义的时候需要这样定义:
@interface XXXConstantNotificationNames : NSObject
@property(nonatomic, readonly) NSString *XXXUserDidLoginNotificationName;
@end
@interface XXXConstant : NSObject
@property(nonatomic, class, copy) XXXConstantNotificationNames *notificationNames;
@end
并且,类常量是不会被 synthesize 的,也就是说编译器不会自动为类常量创建相应的变量,所以在实现文件中,我们需要这么写
@implementation XXXConstantNotificationNames
- (NSString *)XXXUserDidLoginNotificationName {
return @"XXXUserDidLoginNotificationName";
}
@end
@implementation XXXConstant
static XXXConstantNotificationNames *_notificationNames = nil;
+ (void)load {
_notificationNames = [[XXXConstantNotificationNames alloc] init];
}
- (XXXConstantNotificationNames *) {
reutrn _notificationNames;
}
@end
看上去比定义一个 kXXXUserDidLoginNotificationName
字符串常量,麻烦了非常多,但是相信在项目代码量不断增加,以及工程变得越来越复杂以后,这样的做法对于代码管理上是非常有帮助的