23. 通过委托与数据源协议进行对象间通信
委托模式主旨是:定义一套接口,某对象若想接受另一个对象的委托,则需要遵从此接口,以便成为其“委托对象”(delegate)。而这“另一个对象”则可以给其委托对象回传一些信息,也可以在发生相关事件时通知委托对象。
如果有些是可选实现的方法一定要标注optional,然后在使用[self.delegate xxx]之前需要先判断是不是delegate实现了这个可选方法(responseToSelector)。
方法名同样也要清晰明确,并且第一个参数应该把定义delegate的对象传回去,因为有的时候可能两个object用同一个实现了的delegate的对象,这样就可以在方法内区分是哪个object触发了delegate方法。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if(tableView == tableViewA){
……
}else if(tableView == tableViewA){
……
}
}
在实现委托模式与数据源模式时,如果协议中的方法是可选的,那么就会写出一大批类似下面这样的代码来:
if ([_delegate respondsToSelector:@selector(someClassDidSomething)]) {
[_delegate someClassDidSomething];
}
但其实只有第一次检查是有意义的,之后每次检查的结果都是不变的,如果这个delegate是类似于网络progress更新会被频繁调用的时候,可以缓存起第一次检查的结果,这样就不用以后每次都查了,一般不会最开始不能响应某个方法突然变得可以响应。
// 头文件
#import <Foundation/Foundation.h>
@class EOCNetworkFetcher;
@protocol EOCNetworkFetcherDelegate <NSObject>
@optional
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data;
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error;
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher didUpdateProgressTo:(float)progerss;
@end
@interface EOCNetworkFetcher : NSObject
@property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate;
@end
// 实现文件
#import "EOCNetworkFetcher.h"
@interface EOCNetworkFetcher() {
// 使用含有位段的结构体
struct {
// 表示占用1个二进制位,可以表示0或1这两个值
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:)];
}
- (void)didSomeThing
{
// 调用delegate的相关方法
if (_delegateFlags.didUpdateProgressTo) {
[_delegate networkFetcher:self didUpdateProgressTo:0.5];
}
}
@end
24. 将类的实现代码分散到便于管理的数个分类之中
类中经常容易填满各种方法,而这些方法的代码则全部堆在一个巨大的实现文件里。如果过于繁杂,可以利用分类category把代码分区。
#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.h时一并引入分类的头文件。虽然稍微有点麻烦,不过分类仍然是一种管理代码的好办法。
这对于某些应该视为私有的方法来说更是极为有用。可以创建名为Private的分类,把这种方法全都放在里面。这个分类里的方法一般只会在类或框架内部使用,而无须对外公布。这样一来,类的使用者有时可能会在查看回溯信息时发现private一词,从而知道不应该直接调用此方法了。这可算作一种编写"自我描述式代码"(self-documenting code)的办法。
在编写准备分享给其他开发者使用的程序库时,可以考虑创建Private分类。经常会遇到这样一些方法: 它们不是公共API的一部分,然而却非常适合在程序库之内使用。此时应该创建Private分类,如果程序库中的某个地方要用到这些方法,那就引入此分类的头文件。而分类的头文件并不随程序库一并公开,于是该库的使用者也就不知道库里面还有这些私有方法了。(库内部公开,但是又不对外公开)
25. 总是为第三方类的分类名称加前缀
如果类中本来就有此方法,而分类又实现了一次,那么分类中的方法都加入类的方法列表中。如果类中本来就有此方法,而分类又实现了一次,那么分类中的方法会覆盖原来那一份实现代码。实际上可能会发生很多次覆盖,比如某个分类中的方法覆盖了"主实现"中的相关方法,而另外一个分类中的方法又覆盖了这个分类中的方法。多次覆盖的结果以最后一个分类为准。这样就完全取决于分类的加载时机了,非常不靠谱很容易出错,命名加了分类但是方法没有被调用。
要解决此问题,一般的做法是: 以命名空间来区别各个分类的名称与其中所定义的方法。想在Objective-C中实现命名空间功能,只有一个办法,就是给相关名称都加上某个共用的前缀。与给类名加前缀时所应考虑的因素相似,给分类所加的前缀也要选得恰当才行。一般来说,这个前缀应该与应用程序或程序中其他地方所用的前缀相同。于是,我们可以给NSString分类加上ABC前缀:
@interface NSString (ABC_HTTP)
// Encode a string with URL encoding
- (NSString*)abc_urlEncodedString;
// Decode a URL encoded string
- (NSString*)abc_urlDecodedString;
@end
而且如果不加前缀,可能覆盖苹果自己的方法,从而造成难于查找的bug。
在给系统类或者第三方类库加category的时候,同理为了避免同名方法,都应该加上前缀。
26. 勿在分类中声明属性
为了用分类做代码分块(参考24),我们可能把相关属性的声明也放到category里面,那么为了不报错就要自己实现setter/getter,并且通过关联将属性值存起来,如果属性越多,这样的冗余代码越多,也不方便属性的内存管理。
所以正常而言,如果用category做代码分类,属性也应该全部声明在主类中,不要在分类文件中声明,至于分类机制,则应将其理解为一种手段,目标在于扩展类的功能,而非封装数据。
27. 使用"class-continuation分类"隐藏实现细节
"class-continuation分类"是一种无名的分类,它必须定义在其所接续的那个类的实现文件里。其重要之处在于,这是唯一能声明的实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应该定义在类的主实现文件里。与其他分类不同,"class-continuation分类"没有名字。比如,有个类叫做EOCPerson,其"class-continuation分类"写法如下:
@interface EOCPerson ()
//Methods here
@end
其中可以定义方法和实现变量,主要是为了隐藏不希望外部知道的方法和变量,当然你也可以在实现中声明不对外的变量,这个看偏好了,只是一般都是在class-continuation分类中声明会好一点。
@interface ECOPerson() {
NSString *_anInstanceVariable;
}
//Method declarations here
@end
等价于:
@implementation EOCPerson {
int _anotherInstanceVariable;
}
//Method implementations here
@end
※ OC与c++混编
Objective-C++是Objective-C与C++的混合体,其代码可以用这两种语言来编写。由于兼容性原因,游戏后端一般用C++来写。另外,有时候要使用的第三方库可能只有C++绑定,此时也必须使用C++来编码。
假设某个类打算这样写:
#import <Foundation/Foundation.h>
#include "SomeCppClass.h"
@interface EOCClass : NSObject {
@private
SomeCppClass _cppClass;
}
@end
该类的实现文件可能叫做EOCClass.mm,其中.mm扩展名表示编译器应该将此文件按Objective-C++来编译,否则,就无法正确引入SomeCppClass.h了。然而请注意,名为SomeCppClass的这个C++类必须完全引入,因为编译器要完整地解析其定义方能得知_cppClass实例变量的大小。于是,只要是包含EOCClass.h的类,都必须编译为Objective-C++才行,因为它们都引入了SomeCppClass类的头文件。这很快就会失控,最终导致整个应用程序全部都要编译为Objective-C++。
这么做确实完全可行,不过笔者觉得相当别扭,尤其是将代码发布为程序库供其他应用程序使用时,更不应该如此。要求第三方开发者将其源文件扩展名均改为.mm不是很合适。
你可能认为解决此问题的办法是:不引入C++类的头文件,只是向前声明该类,并且将实例变量做成指向此类的指针。
#import <Foundation/Foundation.h>
class SomeCppClass;
@interface EOCClass : NSObject {
@private
SomeCppClass *_cppClass;
}
@end
现在实例变量必须是指针,若不是,则编译器无法得知其大小,从而会报错。但所有指针的大小确实都是固定的,于是编译器只需知道其所指的类型即可。不过,这么做还是会遇到刚才那个问题,因为引入EOCClass头文件的源码里都包含class关键字,而这是C++的关键字,所以仍然需要按Objective-C++来编译才行。这样做既别扭又无必要,因为该实例变量毕竟是private的,其他类为什么要知道它呢?这个问题还是得用"class-continuation分类"来解决。将刚才那个类改写之后,其代码如下:
#import <Foundation/Foundation.h>
@interface EOCClass : NSObject
@end
// EOCClass.mm
#import "EOCClass.h"
#include "SomeCppClass.h"
@interface EOCClass () {
SomeCppClass _cppClass;
}
@end
@implementation EOCClass
@end
改写后的EOCClass类,其头文件里就没有C++代码了,使用头文件的人甚至意识不到其底层实现代码中混有C++成分。某些系统库用到了这种模式,比如网页浏览器框架WebKit,其大部分代码都以C++编写,然而对外展示出来的却是一套整洁的Objective-C接口。CoreAnimation里面也用到了此模式,它的许多后端代码都用C++写成,但对外公布的却是一套纯Objective-C接口。
"class-continuation分类"还有一种合理用法,就是将public接口中声明为"只读"的属性扩展为"可读写",以便在类的内部设置其值。我们通常不直接访问实例变量,而是通过设置访问方法来做(参见第7条),因为这样能够触发"键值观测"(Key-Value Observing, KVO)通知,其他对象有可能正监听此事件。
出现在"class-continuation分类"或其他类中的属性必须同类接口里的属性具备相同的特质(attribute),不过,其"只读"状态可以扩充为"可读写"。
//.m文件
#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
//.h文件
@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
只会在类的实现代码中用到的私有方法也可以声明在"class-continuation分类"中。这么做比较合适,因为它描述了那些只在类实现代码中才会使用的方法。这些方法可以这样写:
@interface EOCPerson()
- (void)p_privateMethod;
@end
然而像上面这样在"class-continuation分类"中声明一下通常还是有好处的,因为这样做可以把类里所含的相关方法都统一描述于此。
另外,若想使类所遵循的协议不为人所知,则可于"class-continuation分类"中声明。
28. 通过协议提供匿名对象
@property (nonatomic, weak) id <EOCDelegate> delegate;
由于该属性的类型是id<EOCDelegate>,所以实际上任何类的对象都能充当这一属性,即便该类不继承自NSObject也可以,只要遵循EOCDelegate协议就行。对于具备此属性的类来说,delegate就是"匿名的"(anonymous)。
如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示,来隐藏类型名称(或类名)。
协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应实现的方法。
-
例如当设计一个兼容各种数据库mysql,PostgreSQL等的manager,返回一个数据库连接,可能是各种底层数据库的类不是很确定,这种时候就可以用id来替代返回的connection类型。
创建匿名对象把这些第三方类简单包裹一下,使匿名对象成为其子类,并遵从EOCDatabaseConnection协议。然后,用"connectionWithIdentifier:"方法来返回这些类对象。在开发后续版本时,无须改变公共API,即可切换后端的实现类。
网友评论