Last week’s post was about a few common networking patterns. I’ve never been satisfied with any of them, so I wanted to describe how I might structure things. I’m assuming a JSON API over HTTP, but these principles are flexible enough to work with any setup.
What are the goals I have for a networking infrastructure?
I want skimmability, simplicity, composability, and lots of flexibility. Let’s take the first two at once. What I want is a single file that describes how a request works. A new developer can glance at that file and understand exactly what that request entails and how it’s structured.
I laid out this pattern in Prefer Composition to Inheritance: the request template.
@protocol SKRequestTemplate
@property (readonly) NSURL *baseURL;
@optional
@property (readonly) NSString *method;
@property (readonly) NSString *path;
@property (readonly) NSDictionary *parameters;
@property (readonly) NSDictionary *headers;
@end
You can define a really easy-to-skim object that represents a single request. We have the most fundamental element of the networking request represented as an object. We can’t break this down into any smaller objects, and that means it’s simple enough.
If I were writing this code in Swift, it would almost definitely be a struct, since it’s a just a dumb holder of data. You can keep these objects around as long or short as you like, since all they do is store information. (You can take a look at a fully implemented request here.)
Because it’s just a protocol, these objects are super flexible. You can bring your own inheritance heirarchy if you like, for example inheriting from one template that has a base URL built-in and maybe some helper methods.
But these objects can’t send any data over the network. To do that, we could we could be lazy and pass the template to some kind of global sender:
[[SKRequestSender sharedSender] sendRequestWithTemplate:loginTemplate];
But that wouldn’t be in the spirit of this blog. What would be awesome is if we could use decoration to add a -sendRequest
method to this template.
@implementation SKSendableRequest
- (instancetype)initWithRequestTemplate:(id<SKRequestTemplate>)template {
self = [super init];
if (!self) return nil;
_template = template;
return self;
}
- (void)sendRequestWithSuccessBlock:(void (^)(id result))successBlock failureBlock:(void (^)(NSError *error))failureBlock {
NSURLRequest *request = //generate a request from a template
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
//handle response
}] resume];
}
@end
As I wrote this object, I saw that there are two complicated steps that I wanted to break out into their own objects. One is building the NSURLRequest
object, and the other is parsing the response.
Request
To generate the request, I made SKRequestBuilder
. This class takes a request template and vends an NSURLRequest
. Creating the request was originally a method on the SKSendableRequest
class, until it started getting gnarly, at which point I graduated it to its own class.
@implementation SKRequestBuilder
- (instancetype)initWithRequestTemplate:(id<SKRequestTemplate>)template {
self = [super init];
if (!self) return nil;
_template = template
return self;
}
- (NSData *)HTTPBody {
return [NSJSONSerialization dataWithJSONObject:self.template.parameters options:0 error:nil];
}
- (NSURL *)URL {
NSURLComponents *URLComponents = [NSURLComponents componentsWithURL:self.template.baseURL resolvingAgainstBaseURL:YES];
NSString *path = [URLComponents.path stringByAppendingString:self.template.path];
URLComponents.path = path;
return URLComponents.URL;
}
- (NSURLRequest *)URLRequest {
NSMutableURLRequest *mutableURLRequest = [[NSMutableURLRequest alloc] init];
mutableURLRequest.HTTPMethod = self.template.method;
mutableURLRequest.HTTPBody = [self HTTPBody];
mutableURLRequest.URL = [self URL];
[self.template.headers enumerateKeysAndObjectsUsingBlock:^(NSString *fieldName, NSString *value, BOOL *stop) {
[mutableURLRequest addValue:value forHTTPHeaderField:fieldName];
}];
return [mutableURLRequest copy];
}
@end
For simplicity, I’ve left out a few details here (like nil checks and query parameter handling). I’ll link to a gist at the end of the post which has handles more stuff.
This also changes the SKSendableRequest
initializer to create the request builder from the template:
- (instancetype)initWithRequestTemplate:(id<SKRequestTemplate>)template {
self = [super init];
if (!self) return nil;
_requestBuilder = [[SKRequestBuilder alloc] initWithRequestTemplate:template];
return self;
}
Response
For parsing the response, we need an object that takes an NSURLResponse
, NSData
, NSError
and parses it into a result and an error, which are what are returned to the completion blocks.
@implementation SKResponseHandler
- (instancetype)initWithResponse:(NSURLResponse *)response data:(NSData *)data error:(NSError *)error {
self = [super init];
if (!self) return nil;
_response = response;
_data = data;
_error = error;
_result = [NSJSONSerialization JSONObjectWithData:self.data options:0 error:nil];
return self;
}
@end
There’s one more thing we need to handle. Requests aren’t necessarily going to want to return the error from NSURLSession
or the JSON result. They might want to use the JSON to generate a local domain object, or they might want to extract errors from the JSON response.
To that end, I made the error
and result
properties of SKResponseHandler
mutable. I also added one more method to the request template, called -finalizeWithResponse:
. The template can implement that method to transform the result
or error
of the response before its returned to the completion block. For example, a login request might implement that method like so:
- (void)finalizeWithResponse:(id<SKResponse>)response {
response.result = [[SKSession alloc] initWithDictionary:response.result];
}
This way, all the logic tied to a particular endpoint is located in one class, and any class using it gets fully baked objects in their completion block.
Our final -sendRequest
method looks like this (again, slightly simplified):
- (void)sendRequestWithSuccessBlock:(void (^)(id result))successBlock failureBlock:(void (^)(NSError *error))failureBlock {
[[[NSURLSession sharedSession] dataTaskWithRequest:self.requestBuilder.URLRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
SKResponseHandler *responseHandler = [[SKResponseHandler alloc] initWithResponse:response data:data error:error];
[self finalizeWithResponse:responseHandler];
if (responseHandler.error) {
if (failureBlock) {
failureBlock(responseHandler.error);
}
} else {
if (successBlock) {
successBlock(responseHandler.result);
}
}
}] resume];
}
One of the big benefits to separating request building and response handling into their own objects is that users can inject whatever request builder they want. If their data comes back in MessagePack format, they can bring their own response handler. The NSURLSession
could also easily be injected.
You could easily create an initializer that accepts a request builder -initWithRequestBuilder:
, but injecting a response handler is a bit harder, since it’s initialized with the response data. To get around this, you could inject a class to be intialized. This is a tough problem and I don’t have a great solution for it yet.
I’ve packaged up this code into a gist. I’m not sure if this code will become a library, but there’s a pretty good chance it’ll end up as the networking layer for Instant Cocoa.