第四章 协议与分类
Objective-C的“协议”(protocol)与java的“接口”类似。Objective-C不支持多重继承,因而我们把某个类应该实现的一系列方法定义在协议里。协议最常见的用途是实现委托模式。
分类(Category)也是Objective-C的一项重要语言特性。利用分类机制,我们无需继承子类即可直接为当前类添加方法。由于Objective-C运行期系统是高度动态的,所以才能支持这一特性,然而,其中也隐藏着一些陷阱,因此在使用分类之前,应该先理解它。
23.通过委托与数据源协议进行对象间通信
Objective-C开发者广泛使用一种名叫“委托模式”(Delegate pattern)的编程设计模式来实现对象间的通信,该模式的主旨是:定义一套接口,某对象若想接受另一个对象的委托,则需遵从此接口,以便成为其“委托对象”(delegate)。而这“另一个对象”则可以给其委托对象回传一些信息,也可以在发生相关事件时通知委托对象。在OObjective-C中,一般通过“协议”这项语言特性来实现此模式,整个Cocoa系统框架都是这么做的。
举个例子,假设要编写一个从网上获取数据的类。此类也许要从远程服务器的某个资源里获取数据。那个远程服务器可能过很长时间才会应答,而在获取数据的过程中阻塞应用程序则是一种非常糟糕的做法。于是,在这种情况下,我们通常会使用委托模式:获取网络数据的类含有一个“委托对象”,在获取完数据之后,它会回调这个委托对象。下图演示了回调委托对象的流程:
利用协议机制,很容易就能以Objective-C代码实现此模式。上图演示的这种情况下,协议可以这样来定义:
@protocol EOCNetworkFetcherDelegate <NSObject>
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
didReceiveData:(NSData *)data;
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
didFailWithError:(NSError *)error;
@end
委托协议名通常是在相关类名后面加上Delegate一词,整个类名采用“驼峰法”来写。
有了这个协议之后,类就可以用一个属性来存放其委托对象了。在本例中,这个类就是EOCNetworkFetcher,于是,此类的接口可以写成这样:
@interface EOCNetworkFetcher : NSObject
@property(nonatomic,weak)id<EOCNetworkFetcherDelegate> delegate;
@end
一定要注意:这个属性需定义成weak,而非strong,因为两者之间必须为“非拥有关系”。通常情况下,扮演delegate的那个对象也要持有本对象。这样做是为了防止造成循环引用。本类中存放的委托对象的这个属性要么定义成weak,要么定义成unsafe_unretained。如果需要在相关对象销毁时自动清空,则定义成weak;若不需要自动清空,则定义为unsafe_unretained。下图演示了本对象与委托对象之间的所有权关系:
实现委托对象的办法是声明某个类遵从委托协议,然后把协议中想实现的那些方法在类里实现出来。某类若要遵从委托协议,可以在其接口中声明,也可以在“calss-continuation分类”中声明。如果要向外界公布此类实现了某协议,那么就在接口中声明,而如果这个协议是个委托协议的话,那么通常只会在类的内部使用。一般都是在“calss-continuation分类”中声明的:
@interface EOCDataModel ()<EOCNetworkFetcherDelegate>
@end
@implementation EOCDataModel
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
didReceiveData:(NSData *)data{
/* Handle data */
}
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
didFailWithError:(NSError *)error{
/* Handle error */
}
@end
委托协议中的方法一般都是“可选的”(optional),因为扮演“受委托者”角色的这个对象未必关心其中的所有方法。为了指明可选方法,委托协议经常使用@optional关键字来标注其大部分或全部的方法:
@protocol EOCNetworkFetcherDelegate <NSObject>
@optional
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
didReceiveData:(NSData *)data;
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
didFailWithError:(NSError *)error;
@end
** 如果要在委托对象上调用可选方法,那么必须提前使用类型信息查询方法判断这个委托对象能否响应相关选择子。**以EOCNetworkFetcher为例,应该这样写:
NSData *data = /* data obtained from network */;
if([_delegate respondsToSelector:
@selector(networkFetcher:didReceiveData:)]){
[_delegate networkFetcher:self didReceiveData:data];
}
这段代码用“respondsToSelector:”来判断委托对象是否实现了相关方法。如果实现了,就调用,如果没实现,就不执行任何操作。这样的话,delegate对象就可以完全按照其需要来实现委托协议中的方法了,不用担心因为哪个方法没实现而导致程序出问题。即便没有设置委托对象,程序也能照常运行,因为给nil发送消息将使if语句的值成为false。
delegate对象中的方法名也一定要起得很恰当才行。方法名应该准确描述当前发生的事件以及delegate对象为何要获知此事件。在本例中,delegate对象里的方法名读起来非常清晰,表明某个“网络数据获取器”对象刚刚接收到某份数据。正如上一段代码所示,在调用delegate对象中的方法时,总是应该把发起委托的实例也一并传入方法中,这样,delegate对象在实现相关方法时,就能根据传入的实例分别执行不同的代码了。比方说可以这样写:
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
didReceiveData:(NSData *)data{
if(fetcher==_myFetcherA){
/* Handle data */
}else if (fetcher==_myFetcherB){
/* Handle data */
}
}
delegate里的方法也可以用于从获取委托对象中获取信息。比方说,EOCNetworkFetcher类也许想提供一种机制:在获取数据时如果遇到了“重定向”,那么将询问其委托对象是否应该发生重定向。delegate对象中的相关方法可以写成这样:
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
shouldFollowRedirectToURL:(NSURL *)url;
也可以用协议定义一套接口,令某类经由该接口获取其所需的数据,委托模式的这一用法旨在向类提供数据,故而又称“数据源模式”(Data Source Pattern)。在此模式中,信息从数据源流向类;而在常规的委托模式中,信息则从类流向受委托者。
下图演示了这两条信息流:
比方说,用户界面框架中的“列表视图”对象可能会通过数据源协议来获取要在列表中显示的数据。除了数据源之外,列表视图还有一个受委托者,用于处理用户与列表的交互操作。将数据源协议与委托协议分离,能使接口更加清晰,因为这两部分的逻辑代码也分开了。另外,“数据源”与“受委托者”可以是两个不同的对象。然而一般情况下,都用同一个对象来扮演这两种角色。
在实现委托模式与数据源模式时,如果协议中的方法是可选的,那么就会写出一大批类似下面这样的代码来:
if([_delegate respondsToSelector:@selector(someClassDidSomething)]){
[_delegate someClassDidSomething];
}
很容易用代码查出某个委托对象是否能响应特定的选择子,可是如果频繁执行此操作的话,那么除了第一次监测的结果有用之外,后续的监测可能都是多余的。如果委托对象本身没变,那么不太可能会突然响应某个原来不能响应的选择子,也不太会突然无法响应某个原来可以响应的选择子。鉴于此,我们通常把委托对象能否响应某个协议方法这一信息缓存起来,以优化程序效率。假设在“网络数据获取器”那个例子中,delegate对象所遵从的协议里有个表示数据获取进度的回调方法,每当数据获取有进度时,委托对象就会得到通知。这个方法在网络数据获取器的生命周期里会多次调用,如果每次都检查委托对象是否能响应此选择子,那就显得多余了。
扩充之后的delegate:
@protocol EOCNetworkFetcherDelegate <NSObject>
@optional
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
didReceiveData:(NSData *)data;
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
didFailWithError:(NSError *)error;
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
didUpdateProgressTo:(float)progress;
@end
将方法响应能力缓存起来的最佳途径是使用“位段”(bitfield)数据类型。这是一项乏人问津的C语言特性,但在此处用起来却正合适。我们可以把结构体摸个字段所占用的二进制位个数设为特定的值。比如像这样:
struct data{
unsigned int fieldA:8;
unsigned int fieldB:4;
unsigned int fieldC:2;
unsigned int fieldD:1;
};
在结构体中,fieldA位段占用8个二进制位,filedB占用4个,fieldC占用2个,fieldD占用1个。于是,fieldA可以表示0至255之间的值,而filedD则可以表示0或1这两个值。我们可以像fieldD这样,把委托对象是否实现了协议中的相关方法这一信息缓存起来。如果创建的结构体中只有大小为1的位段,那么就能把许多Boolean值塞入一小块数据里面了。以网络数据获取器为例,可以在该实例中嵌入一个含有位段的结构体作为其实例变量,而结构体中的每个位段则表示delegate对象是否实现了协议中的相关方法。此结构体的用法如下:
@interface EOCNetworkFetcher ()
{
struct{
unsigned int didReceiveData:1;
unsigned int didFailWithError:1;
unsigned int didUpdateProgressTo:1;
}_delegateFlags;
}
@end
这个结构体用来缓存委托对象是否能响应特定的选择子。实现缓存功能所用的代码可以写在delegate属性所对应的设置方法里:
-(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:)];
}
```
这样的话,每次调用delegate的相关方法之前,就不用检测委托对象是否能响应给定的选择子了,而是直接判断查询结构体里的标志:
```
if(_delegateFlags.didUpdateProgressTo){
[_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
}
```
在相关方法要调用很多次时,值得进行这种优化。是否需要优化,则应依照具体代码来定。如果要频繁通过数据源协议从数据源中获得多份相互独立的数据,那么这项优化技术极有可能会提高程序效率。
**要点:**
* **委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象。**
* **将委托对象应该支持的接口定义成协议,在协议中把可能需要处理的事件定义成方法。**
* **当某对象需要从另外一个对象中获取数据时,可以使用委托模式。这种情况下,该模式亦称“数据源协议”。**
* **若有必要,可实现含有位段的结构体,将委托对象是否能响应相关协议方法这一信息缓存至其中。**
## 24.将类的实现代码分散到便于管理的数个分类之中
之所以要将类代码打散到分类中还有个原因,就是便于调试:对于某个分类中的所有方法来说,分类名称都会出现在其符号中。例如,“addFriend:”方法的“符号名”(symbol name)如下:
```
-\[ EOCPerson(Friendship) addFriend:\]
```
在调试器的回溯信息中,会看到类似下面这样的内容:
```
frame #2:0x0001c50 Test’- \[EOCPerson(Friendship) addFriend: \]
+ 32 at main.m:46
```
在编写准备分享给其他开发者使用的程序库时,可以考虑创建Private分类。经常会遇到这样一些方法:他们不是公共API的一部分,然而却非常适合在程序库之内使用。此时应该创建Private分类,如果程序库中的某个地方要用到这些方法,那就引入此分类的头文件。而分类的头文件并不随程序库一并公开,于是该库的使用者也就不知道库里还有这些私有方法了。
**要点**
* **使用分类机制把类的实现代码划分成易于管理的小块。**
* **将应该视为“私有”的方法归入名叫Private的分类中,以隐藏实现细节。**
## 25.总是为第三方类的分类名称加前缀
分类机制通常用于向无源码的既有类中新增功能。这个特性极为强大,但在使用时也很容易忽视其中可能产生的问题。这个问题在于:分类中的方法是直接添加在类里面的,它们就好比这个类中的固有方法。将分类方法加入类中这一操作是在运行期系统加载分类时完成的。运行期系统会把分类中所实现的每个方法都加入类的方法列表中。如果类中本来就有此方法,而分类又实现了一次,那么分类中的方法会覆写原来那一份实现代码。实际上可能会发生很多次覆盖,比如某个分类中的方法覆盖了“主实现”中的相关方法,而另外一个分类中的方法又覆盖了这个分类中的方法。多次覆盖的结果以最后一个分类为准。
比方说,要给NSString添加分类,并在其中提供一些辅助方法,用于处理与HTTP URL有关的字符串。你可能会把分类写成这样:
```
@interface NSString (HTTP)
//Encode a string with URL encoding
-(NSString*)urlEncodedString;
//Decode a URL encoded string
-(NSString*)urlDecodedString;
@end
```
现在看起来没什么问题,可是,如果还有一个分类也往NSString里添加方法,那会如何呢?那个分类里可能也有个名叫urlEncodedString的方法,其代码与你所添加的大同小异,但却不能正确实现你所需的功能。那个分类的加载时机如果晚于你所写的这个分类,那么其代码就是把你的那份覆盖掉,这样的话,你在代码中调用urlEncodedString方法时,实际执行的是那个分类里的实现代码。由于其执行结果和你预期的值不同,所以自己所写的那些代码也许就无法正常运行了。这种bug很难追查,因为你可能意识不到实际执行的urlEncodedString代码并不是自己实现的那一份。
要解决此问题,一般的做法是:以命名空间来区别各个分类的名称与其中所定义的方法。想在Objective-C中实现命名空间功能,只有一个办法,就是给相关名称都加上某个共用的前缀。与给类名加前缀时所应考虑的因素类似,给分类所加的前缀也要选得恰当才行。一般来说,这个前缀应该与应用程序或程序库中其他地方所用的前缀相同。
比如:
```
@interface NSString (ABC_HTTP)
//Encode a string with URL encoding
-(NSString*)abc_urlEncodedString;
//Decode a URL encoded string
-(NSString*)abc_urlDecodedString;
@end
```
**要点:**
* **向第三方类中添加分类时,总应给其名称加上你专用的前缀。**
* **向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀。**
## 26.勿在分类中声明属性
属性是封装数据的方式。尽管从技术上说,分类里也可以声明属性,但这种做法还是要尽量避免。原因在于,除了“class-continuation分类”之外,其他分类都无法向类中新增实例变量,因此,它们无法把实现属性所需的实例变量合成出来。
比方说一个表示个人信息的类,你决定用分类机制将其代码分段。那么你可能会设计一个专门处理交友事务的分类,其中所有方法都与操作某人的朋友列表有关。若是不知道刚才讲的那个问题,可能会把代表朋友列表的那项属性也放到Friendship分类里面去了:
```
@interface EOCPerson : NSObject
@property(nonatomic,copy,readonly)NSString *firstName;
@property(nonatomic,copy,readonly)NSString *lastName;
-(instancetype)initWithFirstName:(NSString *)firstName
andLastName:(NSString *)lastName;
@end
@interface EOCPerson (Friendship)
@property(nonatomic,strong)NSArray *firends;
-(BOOL)isFriendWith:(EOCPerson *)person;
@end
```
编译器就会警告:
```
Property ‘friends’ requires method ‘friends’ to be defined - use @dynamic or provide a method implementation in this category
Property ‘friends’ requires method ‘setFriends:’ to be defined -use @dynamic or provide a method implementation in this category
```
这段警告意思是说此分类无法合成与friends属性相关的实例变量,所以开发者需要在分类中为该属性实现存取方法。此时可以把存取方法声明为@dynamic,也就是说,这些方法等到运行期再提供,编译器目前是看不见的。如果决定使用消息转发机制(12条)在运行期拦截方法调用,并提供其实现,那么或许可以采用这种做法。
关联对象(第10条)能够解决在分类中不能合成实例变量的问题。比方说,我们可以在分类中用下面这段代码实现存取方法:
```
#import <objc/runtime.h>
static const char *kFriendsPropertyKey = "kFriendsPropertyKey";
@implementation EOCPerson(Friendship)
-(NSArray *)firends{
return objc_getAssociatedObject(self, kFriendsPropertyKey);
}
-(void)setFirends:(NSArray *)firends{
objc_setAssociatedObject(self,
kFriendsPropertyKey,
firends,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
```
这样做可行,但不太理想。要把相似的代码写很多遍,而且在内存管理问题上容易出错,因为我们在为属性实现存取方法时,经常会忘记遵从其内存管理语义。比方说,你可能通过属性特质修改了某个属性的内存管理语义。而此时还要记得,在设置方法中也得修改设置关联对象时所用的内存管理语义才行。所以说,尽管这个做法不坏,但笔者不推荐。
此外,你可能会选用可变数组来实现friends属性所对应的实例变量。若是这么做,就得在设置方法中将传入的数组参数拷贝为可变版本,而这又成为另外一个编码时容易出错的地方。因此,把属性定义在“主接口”(main interface)中要比定义在分类里清晰的多。
在本例中,正确的做法是把所有属性都定义在主接口里。类所封装的全部数据都应该定义在主接口中,这里是唯一能够定义实例变量(也就是数据)的地方。而属性只是定义实例变量及相关存取方法所用的“语法糖”,所以也应遵循同实例变量一样的规则。至于分类机制,则应将其理解为一种手段,目标在于扩展类的功能,而非封装数据。
虽说如此,但有时候只读属性还是可以在分类中使用的。比方说,要在NSCalendar类中创建分类,以返回包含各个月份名称的字符串数组。由于获取方法并不访问数据,而且属性也不需要由实例变量来实现,所以可像下面这样来实现此分类:
```
@interface NSCalendar (EOC_Additions)
@property(nonatomic,strong,readonly)NSArray *eoc_allMonths;
@end
@implementation NSCalendar (EOC_Additions)
-(NSArray *)eoc_allMonths{
if([self.calendarIdentifier isEqualToString:NSGregorianCalendar]){
return @[@"January",@"February",
@"March",@"April",
@"May",@"June",
@"July",@"August",
@"September",@"October",
@"November",@"December"];
}else if (/*Other callendar identifiers*/){
/*retrun months for other calendar*/
}
}
@end
```
由于实现属性所需的全部方法(在本例中,属性是只读的,所以只需实现一个方法)都已实现,所以不会再为该属性自动合成实例变量了。于是,编译器就不会发出警告信息。然而,即便在这种情况下,也最好不要用属性。属性所要表达的意思是:类中有数据在支持着它。属性是用来封装数据的。在本例中,应该声明一个方法,用以获取月份名称列表:
```
@interface NSCalendar (EOC_Additions)
-(NSArray *)eoc_allMonths;
@end
```
**要点:**
* **把封装数据所用的全部属性都定义在主接口里。**
* **在“class-continuation分类”之外的其他分类中,可以定义存取方法,但尽量不要定义属性。**
## 27.使用“class-continuation分类”隐藏实现细节
“class-continuation分类”和普通的分类不同,它必须定义在其所接续的那个类的实现文件里。其重要之处在于,这是唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应该定义在主实现文件里。与其他分类不同,“class-continuation分类”没有名字。
为什么需要有这种分类呢?因为其中可以定义方法和实例变量。为什么能在其中定义方法和实例变量呢?只因为有“稳固的ABI”(第6条),使得我们无须知道对象大小即可使用它。由于类的使用者不一定需要知道实例变量的内存布局,所以,它们也就未必非得定义在公共接口中了。基于上述原因,我们可以像在类的实现文件里那样,于“class-continuation分类”中给类新增实例变量。只需要在适当位置上多写几个括号,然后把实例变量放进去:
```
@interface EOCPerson ()
{
NSString *_anInstanceVariable;
}
//Method declarations here
@end
@implementation EOCPerson
{
int _anotherInstanceVariable;
}
//Method implementions here
@end
```
公共接口里本来就能定义实例变量。不过,把它们定义在“class-continuation分类”或“实现块”中可以将其隐藏起来,只供本类使用。即便在公共接口里将其标注为private,也还是会泄露实现细节。比方说,你有个绝密的类,不想给其他人知道。假设你缩写的某个分类拥有那个绝密类的实例,而这个实例变量又声明在公共接口里面:
```
#import <Foundation/Foundation.h>
@class EOCSuperSecretClass;
@interface EOCClass : NSObject
{
EOCSuperSecretClass *_secretInstance;
}
@end
```
那么,信息就泄露了,别人就会知道有个名叫EOCSuperSecretClass的类。为解决此问题,可以不把实例变量声明为强类型,而是将其类型由EOCSuperSecretClass* 改为id。然而这么做不够好,因为在类的内部使用此实例时,无法得到编译器的帮助。没必要只因为想对外界隐藏某个内容就放弃编译器的辅助检查功能吧?这个问题可以由“class-continuation分类”来解决。那个代表绝密类的实例可以声明成这样:
```
#import "EOCClass.h"
#import "EOCSuperSecretClass.h"
@interface EOCClass ()
{
EOCSuperSecretClass *_secretInstance;
}
@end
@implementation EOCClass
//Methods here
@end
```
实例变量也可以定义在实现块里,从语法上说,这与直接添加到“class-continuation分类”等效,只是看个人喜好了。这些实例变量并非真的私有,因为在运行期总可以调用某些方法绕过此限制,不过,从一般意义上来说,它们还是私有的。此外,由于没有声明在公共头文件里,所以将代码作为程序库的一部分来发行时,其隐藏程度更好。
**要点:**
* **通过“class-continuation分类”向类中新增实例变量。**
* **如果某属性在主接口中声明为“只读”,而类的内部又要用设置方法修改此属性,那么就在“class-continuation分类”中将其扩展为“可读写”。**
* **把私有方法的原型声明在“class-continuation分类”里面。**
* **若想使类所遵循的协议不为人所知,则可于“class-continuation分类”中声明。**
## 28.通过协议提供匿名对象
协议定义了一系列方法,遵从此协议的对象应该实现它们(如果这些方法不是可选的,那么就必须实现)。于是,我们可以用协议把自己所写的API之中的实现细节隐藏起来,将返回的对象设计为遵从此协议的纯id类型。这样的话,想要隐藏的类名就不会出现在API之中了。若是接口背后有多个不同的实现类,而你又不想指明具体使用哪个类,那么可以考虑用这个办法—因为有时候这些类可能会变,有时候它们又无法容纳于标准的类继承体系中,因而不能以某个公共基类来统一表示。
此概念经常称为“匿名对象(anonymous object)”,这与其他语言中的“匿名对象”不同,在那些语言中,该词是指以内联形式所创建出来的无名类,而此词在Objective-C中则不是这个意思。第23条解释了委托与数据源对象,其中就曾用到这种匿名对象。例如,在定义“受委托者”(delegate)这个属性时,可以这样写:
```
@property (nonatomic,weak)id<EOCDelegate> delegate;
```
由于该属性的类型是id<EOCDelegate>,所以实际上任何类型的对象都能充当这一属性,即便该类不继承自NSObject也可以,只要遵从EOCDelegate协议就行,对于具备此属性的类来说,delegate就是“匿名的”。如有需要,可在运行期查出此对象所属的类型。然而这样做不太好,因为指定属性类型时所写的那个EOCDelegate契约已经表明此对象的具体类型无关紧要了。
NSDictionary也能实际说明这一概念。在字典中,键的标准内存管理语义是“设置时拷贝”,而值的语义则是“设置时保留”。因此,在可变版本的字典中,设置键值对所用的方法的签名是:
```
- (void)setObject:(id)anObject forKey:(id <NSCopying>)aKey
```
表示键的那个参数其类型为id <NSCopying>,作为参数值的对象,它可以是任何类型,只要遵从NSCopying协议就好,这样的话,就能向该对象发送拷贝消息了。这个key参数可以视为匿名对象。与delegate属性一样,字典也不关心key对象所属的具体类,而且它也决不应该依赖于此。字典的对象只要能确定它可以给此实例发送拷贝消息就行了。
处理数据库连接的程序库也用这个思路,以匿名对象来表示从另一个库中所返回的对象。对于处理连接所用的那个类,你也许不想叫外人知道其名字,因为不同的数据库可能要用不同的类来处理。如果没办法令其都继承自同一基类,那么就得返回id类型的东西了。不过我们可以把所有数据库连接都具备的那些方法放在协议中,令返回的对象遵从此协议。协议可以这样写:
```
@protocol EOCDatabaseConnection <NSObject>
-(void)connect;
-(void)disconnect;
-(BOOL)isConnected;
-(NSArray *)performQuery:(NSString *)query;
@end
```
然后,就可以用“数据库处理器”(database handler)单例来提供数据库连接了。这个单例的接口可以写成:
```
#import <Foundation/Foundation.h>
@protocol EOCDatabaseConnection;
@interface EOCDatabaseManager : NSObject
+(id)shareInstance;
-(id<EOCDatabaseConnection>)connectionWithIdentifier:(NSString *)identiier;
@end
```
这样的话,处理数据库连接所用的类的名称就不会泄露了,有可能来自不同框架的那些类现在均可以经由同一个方法来返回了。使用此API的人仅仅要求所返回的对象能用来连接、断开并查询数据库即可。
有时对象类型并不重要,重要的是对象有没有实现某些方法,在此情况下,也可以用这些“匿名类型”来表达这一概念。即便实现代码总是使用固定的类,你可能还是会把它写成遵从某协议的匿名类型,以表示类型在此处并不重要。
**要点:**
* **协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应实现的方法。**
* **使用匿名对象来隐藏类型名称(或类名)。**
* **如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。**
转载请注明出处:[第四章 协议与分类](//www.greatytc.com/p/f6ea2bf6b48d)
_参考:《Effective Objective-C 2.0》_