在 Objective-C 的世界里面經常錯過的一個東西是抽象接口。接口(interface)這個詞通常指一個類的 `.h` 文件,但是它在 Java 程序員眼里有另外的含義: 一系列不依賴具體實現的方法的定義。
在 Objective-C 里是通過 protocol 來實現抽象接口的。因為歷史原因,protocol (作為 Java 接口使用)并沒有在 Objective-C 社區里面廣泛使用。一個主要原因是大多數的 Apple 開發的代碼沒有包含它,而幾乎所有的開發者都是遵從 Apple 的模式以及指南的。Apple 幾乎只是在委托模式下使用 protocol。
但是抽象接口的概念很強大,它計算機科學的歷史中就有起源,沒有理由不在 Objective-C 中使用。
我們會解釋 protocol 的強大力量(用作抽象接口),用具體的例子來解釋:把非常糟糕的設計的架構改造為一個良好的可復用的代碼。
這個例子是在實現一個 RSS 訂閱的閱讀器(它可是經常在技術面試中作為一個測試題呢)。
要求很簡單明了:把一個遠程的 RSS 訂閱展示在一個 tableview 中。
一個幼稚的方法是創建一個 `UITableViewController` 的子類,并且把所有的檢索訂閱數據,解析以及展示的邏輯放在一起,或者說是一個 MVC (Massive View Controller)。這可以跑起來,但是它的設計非常糟糕,不過它足夠過一些要求不高的面試了。
最小的步驟是遵從單一功能原則,創建至少兩個組成部分來完成這個任務:
- 一個 feed 解析器來解析搜集到的結果
- 一個 feed 閱讀器來顯示結果
這些類的接口可以是這樣的:
~~~
@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` 用一個 `NSURL` 來初始化來獲取 RSS 訂閱(在這之下可能會使用 NSXMLParser 和 NSXMLParserDelegate 創建有意義的數據),`ZOCTableViewController` 會用這個 parser 來進行初始化。 我們希望它顯示 parser 接受到的指并且我們用下面的 protocol 實現委托:
~~~
@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 來來處理 RSS 非常完美。view controller 會遵從它的公開的接口:
~~~
@interface ZOCTableViewController : UITableViewController <ZOCFeedParserDelegate>
~~~
最后創建的代碼是這樣子的:
~~~
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;
~~~
到目前你可能覺得你的代碼還是不錯的,但是有多少代碼是可以有效復用的呢?view controller 只能處理 `ZOCFeedParser` 類型的對象: 從這點來看我們只是把代碼分離成了兩個組成部分,而沒有做任何其他有價值的事情。
view controller 的職責應該是“從上顯示一些內容”,但是如果我們只允許傳遞`ZOCFeedParser`的話就不是這樣的了。這就表現了需要傳遞給 View controller 一個更泛型的對象的需求。
我們使用 `ZOCFeedParserProtocol` 這個 protocol (在 ZOCFeedParserProtocol.h 文件里面,同時文件里也有 `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
~~~
注意這個代理 protocol 現在處理響應我們新的 protocol 而且 ZOCFeedParser 的接口文件更加精煉了:
~~~
@interface ZOCFeedParser : NSObject <ZOCFeedParserProtocol>
- (id)initWithURL:(NSURL *)url;
@end
~~~
因為 `ZOCFeedParser` 實現了 `ZOCFeedParserProtocol`,它需要實現所有需要的方法。從這點來看 view controller 可以接受任何實現這個新的 protocol 的對象,確保所有的對象會響應從 `start` 和 `stop` 的方法,而且它會通過 delegate 的屬性來提供信息。所有的 view controller 只需要知道相關對象并且不需要知道實現的細節。
~~~
@interface ZOCTableViewController : UITableViewController <ZOCFeedParserDelegate>
- (instancetype)initWithFeedParser:(id<ZOCFeedParserProtocol>)feedParser;
@end
~~~
上面的代碼片段的改變看起來不多,但是有了一個巨大的提升。view controller 是面向一個協議而不是具體的實現的。這帶來了以下的優點:
- view controller 可以通過 delegate 屬性帶來的信息的任意對象,可以是 RSS 遠程解析器,或者本地解析器,或是一個讀取其他遠程或者本地數據的服務
- `ZOCFeedParser` 和 `ZOCFeedParserDelegate` 可以被其他組成部分復用
- `ZOCViewController` (UI邏輯部分)可以被復用
- 測試更簡單了,因為可以用 mock 對象來達到 protocol 預期的效果
當實現一個 protocol 你總應該堅持 [里氏替換原則](http://en.wikipedia.org/wiki/Liskov_substitution_principle)。這個原則讓你應該取代任意接口(也就是Objective-C里的的"protocol")實現,而不用改變客戶端或者相關實現。
此外這也意味著你的 protocol 不應該關注實現類的細節,更加認真地設計你的 protocol 的抽象表述的時候,需要注意它和底層實現是不相干的,協議是暴露給使用者的抽象概念。
任何可以在未來復用的設計意味著可以提高代碼質量,同時也是程序員的目標。是否這樣設計代碼,就是大師和菜鳥的區別。
最后的代碼可以在這找到。[here](http://github.com/albertodebortoli/ADBFeedReader).