Protocols
Protocols
在 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)。这可以跑起来,但是它的设计非常糟糕,不过它足够过一些要求不高的面试了。
A minimal step forward would be to follow the Single Responsibility Principle and create at least 2 components to do the different tasks:
最小的步骤是遵从单一功能原则,创建至少两个组成部分来完成这个任务:
- 一个 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 一个更泛型的对象的需求。
We modify our feed parser introducing the ZOCFeedParserProtocol
protocol (in the ZOCFeedParserProtocol.h file where also ZOCFeedParserDelegate
will be).
我们使用 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 你总应该坚持 里氏替换原则。这个原则让你应该取代任意接口(也就是Objective-C里的的"protocol")实现,而不用改变客户端或者相关实现。
此外这也意味着你的 protocol 不应该关注实现类的细节,更加认真地设计你的 protocol 的抽象表述的时候,需要注意它和底层实现是不相干的,协议是暴露给使用者的抽象概念。
任何可以在未来复用的设计意味着可以提高代码质量,同时也是程序员的目标。是否这样设计代码,就是大师和菜鸟的区别。
最后的代码可以在这找到。here.
更多建议: