鍍金池/ 教程/ iOS/ Protocols
Case語(yǔ)句
美化代碼
參考資料
對(duì)象間的通訊
命名
條件語(yǔ)句
Protocols
NSNotification
面向切面編程
Categories
代碼組織

Protocols

在 Objective-C 的世界里面經(jīng)常錯(cuò)過(guò)的一個(gè)東西是抽象接口。接口(interface)這個(gè)詞通常指一個(gè)類的 .h 文件,但是它在 Java 程序員眼里有另外的含義: 一系列不依賴具體實(shí)現(xiàn)的方法的定義。

在 Objective-C 里是通過(guò) protocol 來(lái)實(shí)現(xiàn)抽象接口的。因?yàn)闅v史原因,protocol (作為 Java 接口使用)并沒(méi)有在 Objective-C 社區(qū)里面廣泛使用。一個(gè)主要原因是大多數(shù)的 Apple 開(kāi)發(fā)的代碼沒(méi)有包含它,而幾乎所有的開(kāi)發(fā)者都是遵從 Apple 的模式以及指南的。Apple 幾乎只是在委托模式下使用 protocol。

但是抽象接口的概念很強(qiáng)大,它計(jì)算機(jī)科學(xué)的歷史中就有起源,沒(méi)有理由不在 Objective-C 中使用。

我們會(huì)解釋 protocol 的強(qiáng)大力量(用作抽象接口),用具體的例子來(lái)解釋:把非常糟糕的設(shè)計(jì)的架構(gòu)改造為一個(gè)良好的可復(fù)用的代碼。

這個(gè)例子是在實(shí)現(xiàn)一個(gè) RSS 訂閱的閱讀器(它可是經(jīng)常在技術(shù)面試中作為一個(gè)測(cè)試題呢)。

要求很簡(jiǎn)單明了:把一個(gè)遠(yuǎn)程的 RSS 訂閱展示在一個(gè) tableview 中。

一個(gè)幼稚的方法是創(chuàng)建一個(gè) UITableViewController 的子類,并且把所有的檢索訂閱數(shù)據(jù),解析以及展示的邏輯放在一起,或者說(shuō)是一個(gè) MVC (Massive View Controller)。這可以跑起來(lái),但是它的設(shè)計(jì)非常糟糕,不過(guò)它足夠過(guò)一些要求不高的面試了。

A minimal step forward would be to follow the Single Responsibility Principle and create at least 2 components to do the different tasks:

最小的步驟是遵從單一功能原則,創(chuàng)建至少兩個(gè)組成部分來(lái)完成這個(gè)任務(wù):

  • 一個(gè) feed 解析器來(lái)解析搜集到的結(jié)果
  • 一個(gè) feed 閱讀器來(lái)顯示結(jié)果

這些類的接口可以是這樣的:


@interface ZOCFeedParser : NSObject

@property (nonatomic, weak) id <ZOCFeedParserDelegate> delegate;
@property (nonatomic, strong) NSURL *url;

- (id)initWithURL:(NSURL *)url;

- (BOOL)start;
- (void)stop;

@end

@interface ZOCTableViewController : UITableViewController

- (instancetype)initWithFeedParser:(ZOCFeedParser *)feedParser;

@end

ZOCFeedParser 用一個(gè) NSURL 來(lái)初始化來(lái)獲取 RSS 訂閱(在這之下可能會(huì)使用 NSXMLParser 和 NSXMLParserDelegate 創(chuàng)建有意義的數(shù)據(jù)),ZOCTableViewController 會(huì)用這個(gè) parser 來(lái)進(jìn)行初始化。 我們希望它顯示 parser 接受到的指并且我們用下面的 protocol 實(shí)現(xiàn)委托:


@protocol ZOCFeedParserDelegate <NSObject>
@optional
- (void)feedParserDidStart:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedInfo:(ZOCFeedInfoDTO *)info;
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedItem:(ZOCFeedItemDTO *)item;
- (void)feedParserDidFinish:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didFailWithError:(NSError *)error;
@end

用合適的 protocol 來(lái)來(lái)處理 RSS 非常完美。view controller 會(huì)遵從它的公開(kāi)的接口:

@interface ZOCTableViewController : UITableViewController <ZOCFeedParserDelegate>

最后創(chuàng)建的代碼是這樣子的:

NSURL *feedURL = [NSURL URLWithString:@"http://bbc.co.uk/feed.rss"];

ZOCFeedParser *feedParser = [[ZOCFeedParser alloc] initWithURL:feedURL];

ZOCTableViewController *tableViewController = [[ZOCTableViewController alloc] initWithFeedParser:feedParser];
feedParser.delegate = tableViewController;

到目前你可能覺(jué)得你的代碼還是不錯(cuò)的,但是有多少代碼是可以有效復(fù)用的呢?view controller 只能處理 ZOCFeedParser 類型的對(duì)象: 從這點(diǎn)來(lái)看我們只是把代碼分離成了兩個(gè)組成部分,而沒(méi)有做任何其他有價(jià)值的事情。

view controller 的職責(zé)應(yīng)該是“從上顯示一些內(nèi)容”,但是如果我們只允許傳遞ZOCFeedParser的話就不是這樣的了。這就表現(xiàn)了需要傳遞給 View controller 一個(gè)更泛型的對(duì)象的需求。

We modify our feed parser introducing the ZOCFeedParserProtocol protocol (in the ZOCFeedParserProtocol.h file where also ZOCFeedParserDelegate will be).

我們使用 ZOCFeedParserProtocol 這個(gè) protocol (在 ZOCFeedParserProtocol.h 文件里面,同時(shí)文件里也有 ZOCFeedParserDelegate )


@protocol ZOCFeedParserProtocol <NSObject>

@property (nonatomic, weak) id <ZOCFeedParserDelegate> delegate;
@property (nonatomic, strong) NSURL *url;

- (BOOL)start;
- (void)stop;

@end

@protocol ZOCFeedParserDelegate <NSObject>
@optional
- (void)feedParserDidStart:(id<ZOCFeedParserProtocol>)parser;
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didParseFeedInfo:(ZOCFeedInfoDTO *)info;
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didParseFeedItem:(ZOCFeedItemDTO *)item;
- (void)feedParserDidFinish:(id<ZOCFeedParserProtocol>)parser;
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didFailWithError:(NSError *)error;
@end

注意這個(gè)代理 protocol 現(xiàn)在處理響應(yīng)我們新的 protocol 而且 ZOCFeedParser 的接口文件更加精煉了:


@interface ZOCFeedParser : NSObject <ZOCFeedParserProtocol>

- (id)initWithURL:(NSURL *)url;

@end

因?yàn)?ZOCFeedParser 實(shí)現(xiàn)了 ZOCFeedParserProtocol,它需要實(shí)現(xiàn)所有需要的方法。 從這點(diǎn)來(lái)看 view controller 可以接受任何實(shí)現(xiàn)這個(gè)新的 protocol 的對(duì)象,確保所有的對(duì)象會(huì)響應(yīng)從 startstop 的方法,而且它會(huì)通過(guò) delegate 的屬性來(lái)提供信息。所有的 view controller 只需要知道相關(guān)對(duì)象并且不需要知道實(shí)現(xiàn)的細(xì)節(jié)。


@interface ZOCTableViewController : UITableViewController <ZOCFeedParserDelegate>

- (instancetype)initWithFeedParser:(id<ZOCFeedParserProtocol>)feedParser;

@end

上面的代碼片段的改變看起來(lái)不多,但是有了一個(gè)巨大的提升。view controller 是面向一個(gè)協(xié)議而不是具體的實(shí)現(xiàn)的。這帶來(lái)了以下的優(yōu)點(diǎn):

  • view controller 可以通過(guò) delegate 屬性帶來(lái)的信息的任意對(duì)象,可以是 RSS 遠(yuǎn)程解析器,或者本地解析器,或是一個(gè)讀取其他遠(yuǎn)程或者本地?cái)?shù)據(jù)的服務(wù)
  • ZOCFeedParserZOCFeedParserDelegate 可以被其他組成部分復(fù)用
  • ZOCViewController (UI邏輯部分)可以被復(fù)用
  • 測(cè)試更簡(jiǎn)單了,因?yàn)榭梢杂?mock 對(duì)象來(lái)達(dá)到 protocol 預(yù)期的效果

當(dāng)實(shí)現(xiàn)一個(gè) protocol 你總應(yīng)該堅(jiān)持 里氏替換原則。這個(gè)原則讓你應(yīng)該取代任意接口(也就是Objective-C里的的"protocol")實(shí)現(xiàn),而不用改變客戶端或者相關(guān)實(shí)現(xiàn)。

此外這也意味著你的 protocol 不應(yīng)該關(guān)注實(shí)現(xiàn)類的細(xì)節(jié),更加認(rèn)真地設(shè)計(jì)你的 protocol 的抽象表述的時(shí)候,需要注意它和底層實(shí)現(xiàn)是不相干的,協(xié)議是暴露給使用者的抽象概念。

任何可以在未來(lái)復(fù)用的設(shè)計(jì)意味著可以提高代碼質(zhì)量,同時(shí)也是程序員的目標(biāo)。是否這樣設(shè)計(jì)代碼,就是大師和菜鳥(niǎo)的區(qū)別。

最后的代碼可以在這找到。here.