One of the early promises of object-oriented programming, encapsulated in the design of the Smalltalk APIs, was a reduction – or really an encapsulation – of the complexity of code. Many programmers believe that the more complex a method or function is, the harder it is to understand and to maintain. Some developers even use tools to measure the complexity quantitatively, in terms of the number of loops or conditions present in the function’s logic. Get this “cyclomatic complexity” figure too high, and your build fails. Unfortunately many class APIs have been designed that don’t take the complexity of client code into account. Here’s an extreme example: the NSStreamDelegate protocol from Foundation.
-(void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)streamEvent;
This is not so much an abstraction of the underlying C functions as a least-effort adaptation into Objective-C. Every return code the lower-level functionality exposes is mapped onto a code that’s funnelled into one place; this delegate callback. Were you trying to read or write the stream? Did it succeed or fail? Doesn’t matter; you’ll get this one callback. Any implementation of this protocol looks like a big bundle of if
statements (or, more tersely, a big bundle of cases in a switch
) to handle each of the possible codes. The default case has to handle the possibility that future version of the API adds a new event to the list. Whenever I use this API, I drop in the following implementation that “fans out” the different events to different handler methods.
-(void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)streamEvent { switch(streamEvent) { case NSStreamEventOpenCompleted: [self streamDidOpen: stream]; break; //... default: NSAssert(NO, @"Apple changed the NSStream API"); [self streamDidSomethingUnexpected: stream]; break; } }
Of course,
NSStream is an incredibly old class. We’ve learned a lot since then, so modern callback techniques are much better, aren’t they? In my opinion, they did indeed get better for a bit. But then something happened that led to a reduction in the quality of these designs. That thing was the overuse of blocks as callbacks. Here’s an example of what I mean, taken from Game Center’s authentication workflow.
@property(nonatomic, copy) void(^authenticateHandler)(UIViewController *viewController, NSError *error)
Let’s gloss over, for a moment, the fact that simply setting this property triggers authentication. There are three things that could happen as a result of calling this method: two are the related ideas that authentication could succeed or fail (related, but diametrically opposed). The third is that the API needs some user input, so wants the app to present a view controller for data entry. Three things, one entry point. Which do we need to handle on this run? We’re not told; we have to ask. This is the antithesis of accepted object-oriented practice. In this particular case, the behaviour required on event-handling is rich enough that a delegate protocol defining multiple methods would be a good way to handle the interaction:
@protocol GKLocalPlayerAuthenticationDelegate @required -(void)localPlayer: (GKLocalPlayer *)localPlayer needsToPresentAuthenticationInterface: (UIViewController *)viewController; -(void)localPlayerAuthenticated: (GKLocalPlayer *)localPlayer; -(void)localPlayer: (GKLocalPlayer *)localPlayer failedToAuthenticateWithError: (NSError *)error; @end
The simpler case is where some API task either succeeds or fails. Smalltalk had a pattern for dealing with this which could be both supported in Objective-C, and extended to cover asynchronous design. Here’s how you might encapsulate a boolean success state with error handling in an object-oriented fashion.
typedef id(^conditionBlock)(NSError **error); typedef void(^successHandler)(id result); typedef void(^failureHandler)(NSError *error); - (void)ifThis:(conditionBlock)condition then:(successHandler)success otherwise:(failureHandler)failure { __block NSError *error; __block id result; if ((result = condition(&error))) success(result); else failure(error); }
Now you’re telling client code whether your operation worked, not requiring that it ask. Each of the conditions is explicitly and separately handled. This is a bit different from Smalltalk’s condition handling, which works by sending the ifTrue:ifFalse: message to an object that knows which Boolean state it represents. The ifThis:then:otherwise: message needs to deal with the common Cocoa idiom of describing failure via an error object – something a Boolean wouldn’t know about.[] However, the Smalltalk pattern *is possible while still supporting the above requirements: see the coda to this post. This method could be exposed directly as API, or it can be used to service conditions inside other methods:
@implementation NSFileManager (BlockDelete) - (void)deleteFileAtPath:(NSString *)path success:(successHandler)success failure:(failureHandler)failure { [self ifThis: ^(NSError **error){ return [self removeItemAtPath: path error: error]?@(1):nil; } then: success otherwise: failure]; } @end int main(int argc, const char * argv[]) { @autoreleasepool { [[NSFileManager defaultManager] deleteFileAtPath: @"/private/tmp" success: ^(id unused){ NSLog(@"Holy crap, you deleted the temporary folder!"); } failure: ^(NSError *error){ NSLog(@"Meh, that didn't work. Here's why: %@", error); }]; } return 0; }
[*]As an aside, there’s no real reason that Cocoa needs to use indirect error pointers. Consider the following API:
-(id)executeFetchRequest:(NSFetchRequest *)fetchRequest
The return value could be an NSArray or an NSError. The problem with this is that in almost all cases this puts some ugly conditional code into the API’s client—though only the same ugly condition you currently have to do in testing the return code before examining the error. This separation of success and failure handlers encapsulates that condition in code the client author doesn’t need to see.
Coda: related pattern
I realised after writing this post that the Smalltalk-esque ifTrue:ifFalse: style of conditional can be supported, and leads to some interesting possibilities. First, consider defining an abstract Outcome class:
@interface Outcome : NSObject - (void)ifTrue:(successHandler)success ifFalse: (failureHandler)failure; @end
You can now define two subclasses which know what outcome they represent and the supporting data:
@interface Success : Outcome + (instancetype)successWithResult: (id)result; @end @interface Failure : Outcome + (instancetype)failureWithError: (NSError *)error; @end
The implementation of these two classes is very similar, you can infer the behaviour of Failure from the behaviour of Success:
@implementation Success { id _result; } + (instancetype)successWithResult: (id)result { Success *success = [self new]; success->_result = result; return success; } - (void)ifTrue:(successHandler)success ifFalse:(failureHandler)failure { success(_result); } @end
But that’s not the end of it. You could use the -ifThis:then:otherwise:
method above to implement a Deferred
outcome, which doesn’t evaluate its result until someone asks for it. Or you could build a Pending result, which starts the evaluation in the background, resolving to success or failure on completion. Or you could do something else.