23.通過委托與數據源協議進行對象間通信
24.將類的實現代碼分散到便於管理的數個分類之中
25.總是為第三方類的分類名稱加前綴
26.勿在分類中聲明屬性
27.使用class-continuation分類隱藏實現細節
28.通過協議提供匿名對象
Objective-C廣泛使用一種名叫委托模式的編程設計模式來實現對象間的通信,該模式的主旨是:定義一套接口,某對象若想接受另一個對象的委托,則需遵從此接口,以便成為其委托對象(delegate)。而這另一個對象則可以給其委托對象回傳一些信息,也可以在發生相關事件時通知委托對象。
比如,用戶界面裡有個顯示一些列數據所用的視圖,那麼,此視圖只應包含顯示數據所需要的邏輯代碼,而不應決定要顯示何種數據以及數據之間如何交互等問題。視圖對象的屬性中,可以包含負責數據與事件處理的對象。這兩種對象分別稱為數據源(data source)和委托(delegate)。
下面是一個使用委托的示例。有一個從網上獲取數據的類,含有一個委托對象,在獲取完數據之後,它會回調這個委托對象。
// 定義協議 @protocol EOCNetworkFetcherDelegate- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didReceiveData:(NSData*)data; - (void)networkFetcher:(EOCNetworkFetcher*)fetcher didFailWithError:(NSError*)error; @end // 獲取網絡數據的類接口 @interface EOCNetworkFetcher : NSObject // 用屬性來存放其委托對象,注意屬性必須定義為weak,否則會形成保留環 @property (nonatomic, weak) id delegate; @end // 委托對象類實現文件 // 擴展中聲明遵循EOCNetworkFetcherDelegate協議 @interface EOCDataModel() @end @implementation EOCDataModel // 實現協議中的方法 - (void)networkFetcher:(EOCNetworkFetcher*)fetcher didReceiveData:(NSData*)data{ // 處理數據 } - (void)networkFetcher:(EOCNetworkFetcher*)fetcher didFailWithError:(NSError*)error{ // 處理錯誤 } @end
委托協議中的方法一般都是可選的,因為受委托的這個對象未必關心其中的所有方法。所以委托協議中進場用@optional關鍵字來標注大部分或全部的方法:
@protocol EOCNetworkFetcherDelegate@optional - (void)networkFetcher:(EOCNetworkFetcher*)fetcher didReceiveData:(NSData*)data; - (void)networkFetcher:(EOCNetworkFetcher*)fetcher didFailWithError:(NSError*)error; @end
如果要在委托對象上調用可選方法,那麼必須提前使用類型信息查詢方法判斷這個委托對象是否能相應相關選擇器。
NSData *data = /* 網絡獲取的數據 */; if([_delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)]){ [_delegate networkFetcher:self didReceiveData:data]; }
但是如果每次都檢查委托對象是否能相應此選擇器顯得有些多余了,可以使用位段數據類型將方法相應能力緩存為結構體中的標志。
// 在擴展中定義結構體 @interface EOCNetworkFetcher(){ struct { unsigned int didReceiveData : 1; unsigned int didFailWithError : 1; }_delegateFlags; } @end @implementation EOCNetworkFetcher - (void)setDelegate:(id)delegate{ _delegate = delegate; // 緩存委托對象相應方法能力 _delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)]; _delegateFlags.didFailWithError = [delegate respondsToSelector:@selector(networkFetcher:didFailWithError:)]; } @end
這樣每次調用delegate相關方法之前就只需要直接查詢標志:
if(_delegateFlags.didReceiveData){ [_delegate networkFetcher:self didReceiveData:data]; }
類中經常容易填滿各種方法,可以通過Objective-C的分類機制,將類代碼按邏輯劃入幾個分區中。
#import// 基本元素(屬性與初始化方法)聲明在主接口中 @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; @end // 負責交友功能的分類 @interface EOCPerson(Friendship) - (void)addFriend:(EOCPerson*)person; - (void)removeFriend:(EOCPerson*)person; - (BOOL)isFriendWith:(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)isFriendWith:(EOCPerson*)person; @end // EOCPerson+Friendship.m #import "EOCPerson+Friendship.h" @implementation EOCPerson(Friendship) // 實現接口中定義的三個方法 @end // 類似地編寫:EOCPerson+Work(.h/.m),EOCPerson+Play(.h/.m)
使用分類不僅讓代碼功能更明確,還有助於調試。對於某個分類中的所有方法來說,分類名稱都會出現在其符號中。根據調試器的回溯信息,可以定缺定位到類中方法所屬的功能區。
在編寫准備分享給其他開發者使用的程序庫時,可以考慮創建Private分類,將應該視為私有的方法歸入名叫Private的分類中,以隱藏細節。
分類機制通常用於向無源碼的既有類中新增功能。這個特性極為強大,但在使用時也很容易忽視其中可能產生的問題:分類中的方法是直接添加到類裡面的,它們就好比這個類中的固有方法,如果類中本來就有此方法,而分類又實現了一次,那麼分類中的方法會覆蓋原來的那一份代碼,實際上甚至有可能發生多次覆蓋。
例如給NSString添加分類,用於處理與HTTP URL有關的字符串。
@interface NSString(HTTP) - (NSString*)urlEncodedString; - (NSString*)urlDecodedString; @end
現在看來沒什麼問題,但是如果又有另一個分類野望NSString中添加名叫urlEncodedString的方法,那麼就會出現覆蓋,導致其中一個方法的代碼無法正常運行。
要解決此問題,一般做法就是通過加前綴來實現命名空間對各個分類的名稱以及其中方法進行區別:
@interface NSString(ABC_HTTP) - (NSString*)abc_urlEncodedString; - (NSString*)abc_urlDecodedString; @end
屬性是封裝數據的方式。盡管從技術上來說,分類裡也可以聲明屬性,但這種做法還是要盡量避免。原因在於,除了class-continuation分類(擴展)之外,其它分類都無法向類中新增實例變量,它們無法把實現屬性所需的實例變量合成出來。
例如以下代碼:
#import@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 // 實現方法 @end // 在分類中聲明了屬性 @interface EOCPerson (Friendship) @property (nonatomic, strong, readonly) NSSet *friends; - (BOOL)isFriendWith:(EOCPerson*)person; @end @implementation EOCPerson (Friendship) // 實現方法 @end
編譯這段代碼時,編譯器會發出警告:此分類無法合成與friends屬性相關的實例變量,所以開發者需要在分類中為該屬性實現存取方法。
此時可以把存取方法聲明為@dynamic,再在運行期用關聯對象解決:
#import "" static const char *kFriendsPropertyKey = "kFriendsPropertyKey"; @implementation EOCPerson (Friendship) @dynamic friends; - (NSSet*)friends{ return objc_getAssociatedObject(self,kFriendsPropertyKey); } - (void)setFriends:(NSSet*)friends{ objc_setAssociatedObject(self,kFriendsPropertyKey,friends,OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end
這樣做可行,但不太理想。要把相似的代碼寫很多遍,而且在內存管理問題上容易出錯,所以最好的辦法還是把所有的屬性都聲明在主接口中。分類的目標在於擴展類的功能,而非封裝數據。
類中經常會包含一些無須對外公布的方法及實例變量。這時候就可以用到擴展(class-continuation分類),擴展和普通的分類不同,它必須定義在其所接續的那個類的實現文件裡。其重要之處在於,這是唯一能聲明實例變量的分類,而且此分類的方法都定義在類的主實現文件裡。
在公共接口裡定義實例變量,即使標注為private,還是會洩漏細節,比如有個絕密的類,不想給他人知道,如果定義在公共接口中:
#import@class EOCSuperSecretClass; @interface EOCClass : NSObject{ @private EOCSuperSecretClass *_secretInstance; } @end
那麼,信息就洩漏了,別人就知道有個名叫EOCSuperSecretClass的類了。如果把實例變量聲明為id,在類內部使用此實例時又無法獲得編譯器的幫助。
這個問題可以用擴展來解決:
#import@interface EOCClass : NSObject{ @end #import "EOCClass.h" #import "EOCSuperSecretClass.h" @interface EOCClass(){ EOCSuperSecretClass *_secretInstance; } @end @implementation EOCClass // code @end
由於沒有聲明在公共頭文件裡,這樣隱藏程度更好。
擴展還可以用於將公共接口中聲明為只讀的屬性擴展為可讀寫的,以便在類的內部設置其值:
// EOCPerson.h @interface EOCPerson : NSObject // 聲明只讀屬性 @property (nonatomic, copy, readonly) NSString *firstName; @property (nonatomic, copy, readonly) NSString *lastName; - (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName; @end // EOCPerson.m @interface EOCPerson () // 將只讀屬性擴展為可讀寫 @property (nonatomic, copy, readwrite) NSString *firstName; @property (nonatomic, copy, readwrite) NSString *lastName; @end
擴展也可以用來聲明只在類實現代碼中才會使用的私有方法:
@interface EOCPerson () - (void)p_privateMethod; @end
擴展的最後一種用法是:若對象遵從了某個秘密協議,不想在公共接口中洩漏這一信息,可以在擴展中聲明
#import "EOCPerson.h" #import "EOCSecretDelegate.h" @interface EOCPerson ()@end @implementation EOCPerson // code @end
協議定義了一系列方法,遵從此協議的對象應該實現它們。於是我們可以用協議把自己所寫的API之中的實現細節隱藏起來,將返回的對象設計為遵從此協議的純id類型。因為接口背後可能有多個不同的實現類,這些類可能會變,有時候它們又無法容納於標准的類繼承體系中,因而不能以某個公共基類來統一表示。
此概念經常稱為匿名對象,在23條委托與數據源對象中,就曾用到這種匿名對象。
@property (nonatomic, weak) iddelegate;
由於該屬性類型是id,所以實際上任何類的對象都能充當這一屬性,即便該類不繼承自NSObject,只要遵循EOCNetworkFetcherDelegate協議就行。
NSDictionary也能實際說明這一概念,在可變版本字典中,設置鍵值對所用的方法是:
- (void)setObject:(id)object forkey:(id)key
表示鍵的那個參數類型為id,字典對象只需要確定它可以給key對象發送拷貝信息就行了,而不關心此實例所屬的具體類。
處理數據庫連接的程序庫也用這個思路,以匿名對象來表示從另一個庫中返回的對象。
@protocol EOCDatabaseConnection - (void)connect; - (void)disconnect; - (BOOL)isConnected; - (NSArray*)performQuery:(NSString*)query; @end #import@protocol EOCDatabaseConnection @interface EOCDatabaseManager : NSObject + (id)sharedInstance; - (id )connectionWithIdentifier:(NSString*)identifier; @end
這樣的話,處理數據庫連接所用的類的名稱就不會洩露了,有可能來自不同框架(如MySQL、PostgreSQL)的那些類現在均可以經由同一個方法來返回了,使用此API的人僅僅要求所返回的對象能用來連接、斷開並查詢數據庫即可。