Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cocoa Callback Design : Best Practice [closed]

I am writing the middleware for a cocoa application, and am debating over how exactly to design callbacks for many long running processes.

When the UI will call a function which executes for a long time, it needs to provide a delegate to allow at least:

  • Report of Success (with return value)
  • Report of Failure (with error value)
  • Report of Progress (completed, expected total)

I have tried a few techniques in the past, shown below

@interface MyClass {
}

//Callback Option1, delgate conforming to protocol
-(void) longRunningProcess2:(id<CallbackProtocol>) delegate;

//Callback Option2, provide a delegate and allow it to provide selectors to callback
-(void) longRunningProcess3:(id) delegate success:(SEL) s1 failure:(SEL) s2 progress:(SEL) s3
@end

For Options 1, the question is then how to phrase the delegate response. The first way I considered is (the function names are minimal for simplicity)

//have a generic callback protocol for every function
@protocol CallbackProtocolGeneric
-(void) success:(id) returnValue;
-(void) failure:(NSError*) error;
@optional
-(void) progress:(NSInteger) completed of:(NSInteger) total;
@end

//have a separate protocol for every function
@protocol CallbackProtocolWithObjectAForOperation1
-(void) objectA:(ObjectA*) objectA operation1SucceedWithValue:(ReturnObject*) value;
-(void) objectA:(ObjectA*) objectA operation1FailedWithError:(NSError*) error;
@optional
-(void) objectA:(ObjectA*) objectA operation1didProgress:(NSInteger) completed of:(NSInteger) total;
@end

In my experience, Callback Option 1 using a generic protocol was difficult to use because if a class wanted to be a callback for multiple operations it could not distinguish which callback it was receiving.

Callback Option2 was cumbersome to use and felt un-natural to use. Plus if the protocol was extended it would require modifying every call.

Callback Option1 using a specific protocol for each process seems to be the most readable and scalable method, however I wonder if making a new protocol for every single function is too verbose (Say a given object has 10+ such 'long operations', then 10 different protocols).

What conclusions have other people come to when implementing such designs?

--edit: In reply to Dave DeLong's answer

I have three classes which have 'long operations', non of the operations in each class or between classes are really related. Some are network resource requests, others are long processing requests.

--edit: A side note, I seem to have a problem where I cannot invoke run loop selectors for messages which have more than one argument. Is this a design limitation or is there a way around this?

For example I have a message such as -(id) someMessage:(id) value1 otherData:(id) value2 moreData:(id) value3

The performSelector functions which queue runLoop's do not support such selectors.

like image 290
Akusete Avatar asked Jul 03 '10 05:07

Akusete


2 Answers

I'd chose longRunningProcess2 instead of longRunningProcess3 just because it's easier to understand if you can see the method declarations on the protocol, as opposed to relying on documentation to work out what the callback method arguments are.

I'd like to add that Apple uses blocks for callbacks in the API new to 10.6, which gives you another option if you aren't supporting 10.5 or earlier.

The blocks approach would look like this:

-(void) longRunningProcessWithSuccessHandler:(void(^)(ReturnObject* value))successHandler
                                errorHandler:(void(^)(NSError* error))errorHandler
                             progressHandler:(void(^)(NSInteger completed, NSInteger total))progressHandler;
{
    NSInteger totalItems = 10;
    NSInteger item = 0;
    for(item = 0; item < totalItems; ++item){
        [self processItem:item];
        progressHandler(item, totalItems);
    }

    BOOL wasSuccessful = ?;
    if(wasSuccessful){
        ReturnObject* value = ?;
        successHandler(value);
    } else {
        NSError* error = ?;
        errorHandler(error);
    }
}

And you would call the method like this:

[SomeObj longRunningProcessWithSuccessHandler:^(ReturnObject* value) { [self showReturnObject:value]; }
                                 errorHandler:^(NSError* error){ [self presentError:error]; }
                              progressHandler:^(NSInteger completed, NSInteger total) { [self updateProgressToPercent:(double)completed/total]; }];
like image 69
Tom Dalling Avatar answered Oct 17 '22 00:10

Tom Dalling


I'd go with a single protocol route, similar to your CallbackProtocolGeneric option, except that I'd expand it to be more like:

- (void) operation:(id)operation didFinishWithReturnValue:(id)returnValue;
- (void) operation:(id)operation didFailWithError:(NSError *)error;
- (void) operation:(id)operation hasCompleted:(NSInteger)progress ofTotal:(NSInteger)total;

So it's like option 1 in that you have a single protocol, but like option 2 in that you're passing back more information. If necessary, you could expand this further with something like:

- (void) operation:(id)operation didFinishStep:(NSInteger)stepNumber withReturnValue:(id)returnValue;
- (void) operation:(id)operation didFailStep:(NSInteger)stepNumber withError:(NSError *)error;
- (void) operation:(id)operation step:(NSInteger)step hasCompleted:(NSInteger)progress ofTotal:(NSInteger)total;

The "step" parameter could be some value that indicates which of the "10+ long operations" that this particular object is making.

Of course, this advice is very generic, since your question is also quite void of specific information, but this is probably the direction I'd go (without knowing more).

like image 31
Dave DeLong Avatar answered Oct 16 '22 23:10

Dave DeLong