Effective Objective-C 2.0: Item 39: Use Handler Blocks to Reduce Code Separation

Item 39: Use Handler Blocks to Reduce Code Separation

A common paradigm in programming a user interface is to perform tasks asynchronously. In this way, the thread that services user interface display and touches does not get blocked when long-running tasks happen, such as I/O or network activity. This thread is often referred to as the main thread. If such methods were synchronous, the user interface would be unresponsive while the task was occurring. In some circumstances, an application may be automatically terminated if it is unresponsive for a certain time. This is true of iOS applications; the system watchdog will terminate an application whose main thread is blocked for a certain period of time.

Asynchronous methods need a way to notify interested parties that they have finished. This can be achieved in several ways. A commonly used technique is having a delegate protocol (see Item 23) to which an object can conform. The object that is the delegate can be notifiedwhen pertinent things happen, such as completion of an asynchronous task.

Consider a class that fetches data from a URL. Using the Delegate pattern, the class may look like this:

#import <Foundation/Foundation.h>

@class EOCNetworkFetcher;
@protocol EOCNetworkFetcherDelegate <NSObject>
- (void)networkFetcher:(EOCNetworkFetcher*)networkFetcher
     didFinishWithData:(NSData*)data;
@end

@interface EOCNetworkFetcher : NSObject
@property (nonatomic, weak)
                      id <EOCNetworkFetcherDelegate> delegate;
- (id)initWithURL:(NSURL*)url;
- (void)start;
@end

A class might use this kind of API as follows:

- (void)fetchFooData {
    NSURL *url = [[NSURL allocinitWithString:
                  @"http://www.example.com/foo.dat"];
    EOCNetworkFetcher *fetcher =
        [[EOCNetworkFetcher allocinitWithURL:url];
    fetcher.delegate = self;
    [fetcher start];
}

// ...

- (void)networkFetcher:(EOCNetworkFetcher*)networkFetcher
     didFinishWithData:(NSData*)data
{
    _fetchedFooData = data;
}

This approach works and is not at all incorrect. However, blocks provide a much cleaner way to achieve the same thing. They can be used to tighten up an API like this and also make it much cleaner for a consumer to use. The idea is to define a block type that is used as the completion handler that is passed directly into the start method:

#import <Foundation/Foundation.h>

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

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

This is very similar to using a delegate protocol but has an added bonus that the completion handler can be defined inline with thestart method call, which greatly improves the readability of code using the network fetcher. For example, consider a class using the completion-block-style API:

- (void)fetchFooData {
    NSURL *url = [[NSURL allocinitWithString:
                  @"http://www.example.com/foo.dat"];
    EOCNetworkFetcher *fetcher =
        [[EOCNetworkFetcher allocinitWithURL:url];
    [fetcher startWithCompletionHandler:^(NSData *data){
        _fetchedFooData = data;
    }];
}

Comparing this to the code that uses the Delegate pattern should make clear that the block approach is much cleaner. The business logic for what happens when the asynchronous task has finished is sitting right next to the code that starts it. Also, because the block is declared in the same scope as the network fetcher is created, you have access to all the variables that are available in that scope. This is not useful in this simple example but can be extremely useful in more complex scenarios.

A downside to the Delegate approach is that if a class were to use multiple network fetchers to download different bits of data, it would need to switch in the delegate method, based on which network fetcher was calling back. Such code may look like this:

- (void)fetchFooData {
    NSURL *url = [[NSURL allocinitWithString:
                  @"http://www.example.com/foo.dat"];
    _fooFetcher = [[EOCNetworkFetcher allocinitWithURL:url];
    _fooFetcher.delegate = self;
    [_fooFetcher start];
}

- (void)fetchBarData {
    NSURL *url = [[NSURL allocinitWithString:
                  @"http://www.example.com/bar.dat"];
    _barFetcher = [[EOCNetworkFetcher allocinitWithURL:url];
    _barFetcher.delegate = self;
    [_barFetcher start];
}

- (void)networkFetcher:(EOCNetworkFetcher*)networkFetcher
     didFinishWithData:(NSData*)data
{
    if (networkFetcher == _fooFetcher) {
        _fetchedFooData = data;
        _fooFetcher = nil;
    } else if (networkFetcher == _barFetcher) {
        _fetchedBarData = data;
        _barFetcher = nil;
    }
    // etc.
}

In addition to making the delegate callback long, this code means that the network fetchers have to be stored as instance variables to be checked against. Doing so might be required anyway, for other reasons, such as to cancel them later, if required; more often than not, it is a side effect that can quickly clog up a class. The benefit of the block approach is that the network fetchers do not have to be stored, and no switching is required. Rather, the business logic of each completion handler is defined along with each fetcher, like this:

- (void)fetchFooData {
    NSURL *url = [[NSURL allocinitWithString:
                  @"http://www.example.com/foo.dat"];
    EOCNetworkFetcher *fetcher =
        [[EOCNetworkFetcher allocinitWithURL:url];
    [fetcher startWithCompletionHandler:^(NSData *data){
        _fetchedFooData = data;
    }];
}

- (void)fetchBarData {
    NSURL *url = [[NSURL allocinitWithString:
                  @"http://www.example.com/bar.dat"];
    EOCNetworkFetcher *fetcher =
        [[EOCNetworkFetcher allocinitWithURL:url];
    [fetcher startWithCompletionHandler:^(NSData *data){
        _fetchedBarData = data;
    }];
}

As an extension of this, many modern block-based APIs also use a block for error handling. Two approaches can be taken. With the first, a separate handler can be used for failure cases to success cases. With the second, the failure case can be wrapped up into the same completion block. Using a separate handler looks like this:

#import <Foundation/Foundation.h>

@class EOCNetworkFetcher;
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

Code that uses this style of API would look like this:

EOCNetworkFetcher *fetcher =
    [[EOCNetworkFetcher allocinitWithURL:url];
[fetcher startWithCompletionHander:^(NSData *data){
    // Handle success
}
                    failureHandler:^(NSError *error){
    // Handle failure
}];

This style is good because it splits up the success and failure cases, meaning that consumer code too is logically split into success and failure, which helps readability. Also, it’s possible for either failure or success to be ignored, if necessary.

The other style, putting the success and failure in a single block, looks like this:

#import <Foundation/Foundation.h>

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

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

Code that uses this style of API would look like this:

EOCNetworkFetcher *fetcher =
    [[EOCNetworkFetcher allocinitWithURL:url];
[fetcher startWithCompletionHander:
    ^(NSData *data, NSError *error){
        if (error) {
            // Handle failure
        } else {
            // Handle success
        }
    }];

This approach requires the error variable to be checked and puts all the logic in one place. The downside is that because all the logic is in one place, the block can become long and complicated. However, the upside of the single-block approach is that it is much more flexible. It’s possible to pass an error, as well as data, for example. Consider that perhaps the network was able to download half the data and then an error occurred. Maybe in that case, you would pass back the data and the associated error. The completion handler can then determine the problem, handle it as appropriate, and may be able to do something useful with the part of the data that was successfully downloaded.

Another good reason for putting success and failure in the same block is that sometimes when processing the data of an otherwisesuccessful response, the consumer detects an error. Perhaps the data returned was too short, for example. This situation may need to be handled in just the same way as the failure case from the network fetcher’s perspective. In that case, having a single block means that processing can be done and that, if an error is found with the data, it can be handled along with the case in which a network fetcher error has been detected. If the success and failure cases are split into separate handlers, it becomes impossible to share the error-handling code of this scenario without deferring to a separate method, which defeats the point of using handler blocks to put business logic code all in one place.

Overall, I suggest using a single handler block for success and failure, which is also the approach that Apple seems to be taking in its APIs. For example, TWRequest from the Twitter framework andMKLocalSearch from the MapKit framework both use the approach of a single handler block.

Another reason to use handler blocks is for calling back at pertinent times. A consumer of the network fetcher may want to be told each time progress is made with the download, for example. This could be done with a delegate. But continuing the theme of using handler blocks instead, you could add a progress-handler block type and a property:

typedef void(^EOCNetworkFetcherCompletionHandler)
                                         (float progress);
@property (nonatomic, copy)
              EOCNetworkFetcherProgressHandler progressHandler;

This is a good pattern to follow, as it yet again allows all the business logic to be put in one place: where the network fetcher is created and the completion handler is defined.

Another consideration when writing handler-based APIs stems from the fact that some code is required to run on a certain thread. For instance, any UI work in both Cocoa and Cocoa Touch must happen on the main threadThis equates to the main queue in GCD-land. Therefore, it is sometimes prudent to allow the consumer of a handler-based API to decide on which queue the handler is run. One such API is NSNotificationCenter, which has a method whereby you can register to be notified of a certain notification by the notification center’s executing a certain block. It is possible, but not compulsory, to decide in what queue to schedule the block. If no queue is given, the default behavior is invoked, and the block is run on the thread that posted the notification. The method to add an observer looks like this:

- (id)addObserverForName:(NSString*)name
                  object:(id)object
                   queue:(NSOperationQueue*)queue
              usingBlock:(void(^)(NSNotification*))block

Here, an NSOperationQueue is passed in to denote the queue that the block should be run on when a notification is fired. Operation queues rather than the lower-level GCD queues are used, but the semantics are the same. (See Item 43 for more about GCD queues versus other tools.)

You could similarly design an API whereby an operation queue is passed in or even a GCD queue, if that is the level at which you want to pitch the API.

Things to Remember

Image Use a handler block when it will be useful to have the business logic of the handler be declared inline with the creation of the object.

Image Handler blocks have the benefit of being associated with an object directly rather than delegation, which often requires switching based on the object if multiple instances are being observed.

Image When designing an API that uses handler blocks, consider passing a queue as a parameter, to designate the queue on which the block should be enqueued.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值