01-禅与 Objective-C 编程艺术之条件语句与命名
Categories
虽然我们知道这样写很丑, 但是我们应该要在我们的category
方法前加上自己的小写前缀以及下划线,比如- (id)zoc_myCategoryMethod
。 这种实践同样被苹果推荐。
这是非常必要的。因为如果在扩展的 category
或者其他 category
里面已经使用了同样的方法名,会导致不可预计的后果。实际上,实际被调用的是最后被加载的那个category
中方法的实现(译者注:如果导入的多个 category
中有一些同名的方法导入到类里时,最终调用哪个是由编译时的加载顺序来决定的,最后一个加载进来的方法会覆盖之前的方法)。
如果想要确认你的分类方法没有覆盖其他实现的话,可以把环境变量 OBJC_PRINT_REPLACED_METHODS
设置为 YES
,这样那些被取代的方法名字会打印到 Console
中。现在 LLVM 5.1 不会为此发出任何警告和错误提示,所以自己小心不要在分类中重载方法。
一个好的实践是在 category
名中使用前缀。
** 例子 **
@interface NSDate (ZOCTimeExtensions)
- (NSString *)zoc_timeAgoShort;
@end
** 不要这样 **
@interface NSDate (ZOCTimeExtensions)
- (NSString *)timeAgoShort;
@end
分类可以用来在头文件中定义一组功能相似的方法。这是在 Apple的 Framework 也很常见的一个实践(下面例子的取自NSDate 头文件)。我们也强烈建议在自己的代码中这样使用。
我们的经验是,创建一组分类对以后的重构十分有帮助。一个类的接口增加的时候,可能意味着你的类做了太多事情,违背了类的单一功能原则。
之前创造的方法分组可以用来更好地进行不同功能的表示,并且把类打破在更多自我包含的组成部分里。
@interface NSDate : NSObject <NSCopying, NSSecureCoding>
@property (readonly) NSTimeInterval timeIntervalSinceReferenceDate;
@end
@interface NSDate (NSDateCreation)
+ (instancetype)date;
+ (instancetype)dateWithTimeIntervalSinceNow:(NSTimeInterval)secs;
+ (instancetype)dateWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti;
+ (instancetype)dateWithTimeIntervalSince1970:(NSTimeInterval)secs;
+ (instancetype)dateWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;
// ...
@end
Protocols
在 Objective-C 的世界里面经常错过的一个东西是抽象接口。接口(interface
)这个词通常指一个类的 .h 文件,但是它在 Java 程序员眼里有另外的含义: 一系列不依赖具体实现的方法的定义。(译者注:在OC中,类的接口对应在.m文件中都会有具体的实现,但Java中接口更接近于OC中的抽象接口或者说协议(protocol
))
在 Objective-C 里是通过 protocol
来实现抽象接口的。因为历史原因,protocol
(使用方法类似java的接口)并没有大量地在Objective-C的代码中使用也没有在社区中普及(指的是那种像Java程序员使用接口那样来使用protocol
的方式)。一个主要原因是大多数的 Apple 开发的代码没有采用这种的方式,而几乎所有的开发者都是遵从 Apple 的模式以及指南。Apple 几乎只是在委托模式下使用 protocol
。
但是抽象接口的概念很强大,在计算机科学的历史中颇有渊源,没有理由不在 Objective-C 中使用。
这里通过一个具体的例子来解释protocol
的强大力量(用作抽象接口):把非常糟糕的设计的架构改造为一个良好的可复用的代码。
这个例子是在实现一个 RSS 阅读器(它可是经常在技术面试中作为一个测试题呢)。
要求很简单:在TableView中展示一个远程的RSS订阅。
一个幼稚的方法是创建一个 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
我要说,这是一个处理RSS业务的完全合理而恰当的protocol
。这个ViewController
在Public接口中将遵循这个protocol
:
@interface ZOCTableViewController : UITableViewController <ZOCFeedParserDelegate>
最后创建的代码是这样子的:
NSURL *feedURL = [NSURL URLWithString:@"http://www.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
,它需要实现所有的required
方法。 从这点来看viewController
能接受任何遵循该协议的对象,只要确保所有的对象都会响应start
和stop
方法并通过delegate
属性提供信息(译者注:因为protocol
默认情况下所有的方法定义都是required
的)。对指定的对象而言,这就是viewController
所要知道的一切,且不需要知道其实现的细节。
@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
的抽象表述时应非常用心,并且要牢记它和它背后的实现是不相干的,真正重要的是协议(这个暴露给使用者的抽象表述)。
任何在未来可复用的设计,无形当中可以提高代码质量,这也应该一直是程序员的追求。是否这样设计代码,就是大师和菜鸟的区别。
最后的代码可以在这里 找到。
NSNotification
当你定义你自己的 NSNotification
的时候你应该把你的通知的名字定义为一个字符串常量,就像你暴露给其他类的其他字符串常量一样。你应该在公开的接口文件中将其声明为 extern
的, 并且在对应的实现文件里面定义。
因为你在头文件中暴露了符号,所以你应该按照统一的命名空间前缀法则,用类名前缀作为这个通知名字的前缀。
同时,用一个 Did/Will
这样的动词以及用 "Notifications
" 后缀来命名这个通知也是一个好的实践。
// Foo.h
extern NSString * const ZOCFooDidBecomeBarNotification
// Foo.m
NSString * const ZOCFooDidBecomeBarNotification = @"ZOCFooDidBecomeBarNotification";