どうもこんにちは。8/27担当のsaitenです。
XCode4.4から新たに新リテラルが使えるようになったりして、Objective-CがどんどんLL化してるなーと思う昨今です。
さて、今回はObjective-Cで書かれたDeferredの実装がぱっと見で見当たらなかったので作ってみました。
Deferredって何
概念的なところはこの辺りを参照。実装的にはDeferredオブジェクトは未完了、完了、失敗のいずれかの状態を持っていて、完了もしくは未完了の状態になると、予め登録しておいたコールバックに対して状態が変わったことを通知する仕組みを持っているといった感じです。
jQuery等でも実装されていて、javascriptな方面では割りとメジャーなんじゃないかと勝手に思っています。
使い方
例えば、こんな感じでHTTPリクエストを行うメソッドがあったとして、
- (void)request:(NSURLRequest*)request completion:(void (^)(NSData*))completionBlock failure:(void (^)(NSError*))failureBlock
{
dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(global_queue, ^{
NSURLResponse *response = nil;
NSError *error = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
if(error) {
failureBlock(error);
} else {
completionBlock(data);
}
});
}
通常はこんな感じで処理を書くと思います。
[self request:request completion:^(NSData *responseData) {
// 成功時処理
} failure:^(NSError *error) {
// 失敗時処理
}];
これはこれで全く問題無いのですが、deferredオブジェクトを使ってみるとどんな感じになるでしょうか。
STDeferredを使う場合はまずrequestメソッドをこんな感じに修正します。
- (STDeferred*)request:(NSURLRequest*)request
{
STDeferred *deferred = [STDeferred deferred];
dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(global_queue, ^{
NSURLResponse *response = nil;
NSError *error = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
if(error) {
[deferred reject:error];
} else {
[deferred resolve:data];
}
});
return deferred;
}
成功時と失敗時の処理をblockで渡していたのをやめ、代わりにSTDeferredオブジェクトを生成して返すように変更しました。HTTPリクエストが完了次第、成功時には生成したdeferredのresolveメソッドを実行し、失敗時にはrejectメソッドを呼び出すように変更しています。
で、これをこんな感じで呼び出します。
[[[self request:request] then:^(id responseData) {
// 成功時処理
}] fail:^(id error) {
// 失敗時処理
}];
request:が返したSTDeferredオブジェクトに対して、then:メソッドで成功(resolve)された場合の処理、fail:メソッドで失敗(reject)された場合の処理を追加する感じですね。
これだけだと何が嬉しいのかわかりませんが、requestの結果に対して、複数の処理を追加したい場合にはこんな感じでじゃんじゃんチェインして書くことができます。
[[[[self request:request] then:^(id responseData) {
// 処理1
}] then:^(id responseData) {
// 処理2
}] then: ^(id responseData) {
// 処理3
}];
また、複数のリクエストの結果がすべて返るのを待ってからある処理をしたいといった場合に、whenメソッドを使うとこんな感じで書けます。
[[STDeferred when:[self request:request1], [self request:request2], nil] then:^(id responseDataArray) {
NSData *data1 = [responseDataArray objectAtIndex:0];
NSData *data2 = [responseDataArray objectAtIndex:1];
:
}];
さらに、複数のリクエストを順々に実行したいといった場合は、pipeメソッドを使って、次のように書けます。
[[[[self request:req1] pipe:^(id resultData) {
return [self request:req2];
}] pipe:^(id resultData) {
return [self request:req3];
}] fail:^(id error) {
// いずれかのrequestでエラーが発生した場合は中断してここに来る
}];
さらにpipeとwhenを組み合わせれば、ある複数の連続したリクエストとを同時に処理させ、それらが両方とも完了次第、処理を実行するといった複雑な処理も、
STDeferred *flow1 = [[[[self request:req1] pipe:^(id resultData) {
return [self request:req2];
}] pipe:^(id resultData) {
return [self request:req3];
}]
STDeferred *flow2 = [[[[self request:req4] pipe:^(id resultData) {
return [self request:req5];
}] pipe:^(id resultData) {
return [self request:req6];
}]
[[[STDeferred when:flow1, flow2, nil] then:^(id result) {
// 成功
}] fail:^(id errors) {
// 失敗
}];
といった感じでシンプルにかけます。
まとめ
Deferredの良い所はこういった時間のかかる処理を抽象化することで、複雑なフローを分かりやすく記述できるところですね。
今回、作ったSTDeferredはgithubに置いてありますのでよければ試してくださいー。
http://github.com/saiten/STDeferred
ではでは。