39.用handler块降低代码分散程度

《编写高质量iOS与OS X代码的52个有效方法》--第六章 第39条
(ps:此乃读书笔记,加深记忆,仅供大家参考)


第39条:用handler块降低代码分散程度

为用户界面编码时,一种常用的范式就是“异步执行任务”(perform task asynchro nously)。这种范式的好处在于:处理用户界面的显示及触摸操作所用的线程,不会因为要执行I/O或网络通信这类耗时的任务而阻塞。这个线程通常称为主线程(main thread)。某些情况下,如果应用程序在一定时间内无响应,那么就会自动终止。“系统监控器”(system watchdog)在发现某个应用程序的主线程已经阻塞了一段时间之后,就会令其终止。

异步方法在执行完任务之后,需要以某种手段通知相关代码。实现此功能有很多办法。常用的技巧是设计一个委托协议(参见第23条),令关注此事的对象遵从该协议。对象成为delegate之后,就可以在相关事件发生时(例如某个异步任务执行完毕时)得到通知了。

如果改用块来写的话,代码会更清晰。块可以令这种API变得更紧致,同时也令开发者调用起来更加方便。

typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);

@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler: (EOCNetworkFetcherCompletionHandler)handler;
@end

这和使用委托协议很像,不过多了个好处,就是可以在调用start方法时直接以内联形式定义completion handler,以此方式来使用“网络数据获取器”(network fetcher),可以令代码比原先易懂很多。

与使用委托模式的代码相比,用块写出来的代码闲的更为整洁。委托模式有个缺点:如果类要分别使用多个获取器下载不同的数据,那么就得在delegate回调方法里根据传入的获取器参数来切换。

异步执行任务完毕后所需运行的业务逻辑,和启动异步任务所用的代码放在了一起。无须保存获取器,也无需再回调方法里切换,每个completion handler的业务逻辑,都是和相关获取器对象一起来定义的。

NSURL *url = [[NSURL alloc] initWithString:@"http:www.baidu.com"];
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data) {
    _fetchedFooData = data;
}];

这种写法还有其他用途,比如,现在很多基于块的API都使用块来处理错误。可以分别用两个处理器来处理操作失败的情况和操作成功的情况。也可以把处理失败情况所需的代码,与处理正常情况所用的代码,都封装到同一个completion handler块里。

采用两个独立的处理程序:

typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
typedef void (^EOCNetworkFetcherErrorHandler)(NSError *error);

@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion failureHandler:(EOCNetworkFetcherErrorHandler)failure;
@end

这种API设计风格很好,由于成功和失败的情况要分别处理,所以调用此API的代码也就会按照逻辑,把应对成功和失败情况的代码分开来写,这将令代码更易读懂。

把处理成功情况和失败情况所用的代码全放在一个块里:

typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data, NSError *error);

@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
@end

此种API调用方式如下:

EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data, NSError *error) {
    if (error) {
        //Handler failure
    } else {
        //Handler success
    }
}];

这种方法需要在块代码中检测传入的error变量,并且要把所有逻辑代码都放在一处。这种写法的缺点是:由于全部逻辑都写在一起,所以会令块变得比较长,且比较复杂。然而只用一个块的写法也有好处,那就是更为灵活。

把成功情况和失败情况放在同一个块中,还有个优点:调用API的代码可能会在处理成功相应的过程中发现错误。比方说,返回的数据可能太短了。这种情况需要和网络数据获取器所认定的失败情况按同一方式处理。此时,如果采用单一块的写法,那么就能把这种情况和所认定的失败情况统一处理了。

总体来说,笔者建议使用同一块来处理成功与失败的情况,苹果公司似乎也是这样设计其API的。例如,Twitter框架中的TWRequest及MapKit框架中的MKLocalSearch都只是用一个handler块。

有时需要在相关时间点执行回调操作,这种情况也可以使用handler块。比方说,调用网络数据获取器的代码,也许想在每次有下载进度时都得到通知。这可以通过委托模式实现。不过也可以使用本节的handler块,把处理下载进度的handler定义成块类型,并新增一个此类型的属性:

typedef void (^EOCNetworkFetcheProgressHandler)(float progress);

@property (nonatomic, copy) EOCNetworkFetcheProgressHandler progressHandler;

这种写法很好,因为它还是能把所有业务逻辑都放在一起,也就是把创建网络数据获取器和定义progress handler所用的代码写在一处。

基于handler来设计API还有个原因,就是某些代码必须运行在特定的线程上。因此,最好能由调用API的人来决定handler应该运行在那个线程上。NSNotificationCenter就属于这种API,它提供了一个方法,调用者可以经由此方法来注册想要接收的通知,等到相关事件发生时,通知中心就会执行注册好的那个块。调用者可以指定某个块应该安排在哪个执行队列里,然而这不是必需的。若没有指定队列,按默认方式执行。

- (id <NSObject>)addObserverForName:(nullable NSString *)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block

要点

  • 在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明。
  • 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用handler块来实现,则可直接将块与相关对象放在一起。
  • 设计API时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • 用 handler 块 降低代码分散程度 为用户界面编码时, 一种常见的范式是 '异步执行任务', 这种范式的好处...
    dingzhijie阅读 572评论 0 1
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,392评论 25 707
  • 有时候,想想生活,其实也不过如此。不在世间营营,就已经知足。闲时就去远方,看看生命和死亡的诗;纵使忙碌也不对生活...
    胡然乎阅读 159评论 0 0
  • 平凡的日子里 忽然有一天 千里之外 有个人对你说 我想你 那么 我很庆幸 我在你的世界里存在过 我也很庆幸 我会被...
    YIERLHY阅读 267评论 0 0