I am currently rewriting the network layer that synchronizes a Core Data database in our iOS app with a variety of web services which I recently placed behind a RESTful facade. Now I can throw away all the nasty SOAP and XML/RPC code and rewrite a nice clean JSON based synchronization framework. Or so I thought… It turns out that switching to JSON came with its own set of problems. One of them caused a not so DRY and smelly repeating pattern of code that started to annoy me to no end, so I had to do something about it. The problem is how JSON (or more likely my server side Jersey/JAXB implementation of it) handles arrays with a single element in them. The behavior I get is this:
- empty lists become null
- lists with one element get represented as just that element, without [ ] enclosure!
- lists with multiple elements behave normally and become a JSON array
So after JSONKit (an awesome and blazing fast JSON parser/encoder) parses the JSON to the usual NSArray and NSDictionary objects, I find myself writing something like this over and over again:
id customers = [data objectForKey:@"customers"];
if ([customers isKindOfClass: [NSDictionary class]]) {
[self syncCustomer: customers];
} else {
for (NSDictionary *customer in customers) {
[self processCustomer: customer];
}
}
Same for reports, topics, subscriptions, issues, etc. you get the idea. Every time we process an array of records we have to check if it happens to be an array of size one in which case it is not an array at all… Not very nice! I don’t know who thought this was a nice “optimization”, but they were wrong!
What if we could just write what we want to do with each record in the array and pass it along to something that would apply it to either the single record or all of them if there are more? This is where blocks come to the rescue once again.
Here is an example code snippet using the solution I came up with:
[self withJSON: [jsonData objectForKey:@"comments"]
do: ^(NSDictionary *comment) {
NSString *commentId = [comment objectForKey:@"id"];
// etc., sync comment with CoreData entity
}];
No more repeated logic to test for one or more elements in the NSArray, this is now all hidden in the withJSON:do: method, which accepts an objective-C block to apply to each record whether they appear in an array or solo. This method looks like this:
-(void)withJSON: (id)records
do: (void (^)(NSDictionary *))block {
if (!records) return;
if ([records isKindOfClass:[NSArray class]]) {
for (id record in records) {
[self withJSON:record do:block];
}
} else if ([records isKindOfClass:[NSDictionary class]]) {
block(records);
}
}
If records is nil, it does nothing and simply returns, if records is an NSArray it iterates over it and recurses for each element in the array, and finally if records is a NSDictionary it invokes the block with the dictionary as a parameter. The nice thing about using recursion here is that we don’t have to repeat the test for “NSDictionary-ness” in the loop and we get support for nested arrays for free!
So, once again blocks have saved my sensitive nose from another nasty code smell!
Alternative implementation using categories
I just thought of (and tested) another way to implement this using a category on NSArray and NSDictionary. Each of these implements the same method jsonDo:.
The NSDictionary implementation simply invokes the block passing itself as the parameter, and the NSArray implementation invokes jsonDo: on each of its elements:
@implementation NSObject (JSONParsing)
-(void)jsonDo: (void (^)(NSDictionary *))block {
// do NOTHING
}
@end
@implementation NSDictionary (JSONParsing)
-(void)jsonDo: (void (^)(NSDictionary *))block {
block(self);
}
@end
@implementation NSArray (JSONParsing)
-(void)jsonDo: (void (^)(NSDictionary *))block {
for (id next in self) {
[next jsonDo:block];
}
}
@end
Why the empty implementation in NSObject? If we did not have that, our code would crash when we receive an array with things other than NSDictionary elements, like NSString or NSNumber, scenarios for which this code was not designed.
The same example used above now looks like this using the alternative implementation:
[[jsonData objectForKey:@"comments"] jsonDo:^(NSDictionary *comment) {
NSString *commentId = [comment objectForKey:@"id"];
// etc. sync with Core Data entity
}];
And once again, objective-C blocks and categories turn out to be a pleasant perfume eliminating some pretty awful code smells!