Networking is a fundamental component of modern apps. Despite the ubiquity of the task I frequently seen network requests handled poorly. In this post I’ll describe the API design and app architecture I use for handling network request.
Before getting to the details of the method signature for a network request we need to discuss where in the apps architecture the network requests should be addressed. The architecture of a Cocoa app is based on the Model-View-Controller pattern. Unfortunately there are many different interpretations of MVC. To ensure we’re on the same page I’ll start by briefly describing my take on the MVC pattern:
Model: the model is the brains of the app. The model is full of moving parts. It is not a static data store that the controller can arbitrarily manipulate. The model is the boss. The model presents its self to the controller as a collection of semantically rich objects that provide the functionality for the app.
View: The view is the human facing component of the system. That generally means visual elements, for example output to the screen; inputs from touches; the pointer and the keyboard, but also other device sensors such as GPS and accelerometers.
Controller: the controller is the connection between the model and the view. The controller is not a connection between the model and all external data sources. The controller is not responsible for fetching, parsing or validating data, that’s the responsibility of the model. The controller simply takes the actions from the view and communicates them to the model and vice versa.
This approach has served me well. the strict delimitation between model and controller is powerful as it forces you to think carefully about what role an object performs. such steadfast conformance to MVC could be viewed as myopic. I, however, have found it to be very powerful constraint that has helped me create app architectures that are easy for new comes to pick up, semantically rich yet flexibly to modification. (I use a 2+1 style for classes prefix. The first 2 letters indicate the app and the 3 indicate where they sit in the MVC pattern. For example BEMPerson
, BEVFancyButton
, BECPersonViewController
).
So which section of the app is responsible for creating network requests? The model. It’s the model that’s responsible for understanding the mundane details of the web service it interacts with. The model should encapsulate these details and provide a semantically rich interface that allows the controller to operate at the higher level of abstraction rather than the be concerned with the nitty-gritty details of networking.
Here’s our headline act:
-(id)fetchPeopleMatchingQuery:(id)query completionHandler:(void^(id progress, BOOL didSucceed, NSArray *results, NSError *error))completionHandler;
The juicy part is completionHandler
. Let’s look at each parameter in turn.
progress
: This object is the key to keeping our sanity. The value of the progress progress
is the same object that is return synchronously. This object performs 2 roles. Firstly it provides a means for the caller to interact with the request (cancel, monitor progress etc) while the request is running (i.e. before completionHandler
is invoked). Secondly it makes it simpler for the caller to determine what to do when completionHandler
is called. Here’s an example that illustrates both functions:
@implementation BECSearchViewController
-(void)performSearch
{
[self.searchProgress cancel]; //1\. Cancel the old request
self.searchProgress = [self.addressBook fetchPeopleMatchingQuery:self.searchQuery completionHandler:^(id progress, BOOL didSucceed, NSArray *results, NSError *error){
//2\. We're only interested in processing the current request
BOOL isCurrentSearch = [self.searchToken isEqual:progress];
if (!isCurrentSearch) return; //The request is not current so is not of interest.
//Store the results and the error to display in the view.
self.searchResults = results;
self.searchError = error;
//Tidy up
self.searchProgress = nil; //Clear the request so when we refresh the view we can provide feedback.
[self refreshView];
}];
But why use a protocol instead of an explicit object? An interface should only expose what is strictly necessary. If, for example, the return type had been an NSOperation
subclass the caller would be able to interfere with its’ completionBlock
and inadvertently cause havoc for the model. By only exposing what is strictly necessary (in this case cancel
) we make such meddling impossible. A protocol also provides flexibility for the model to create disparate implementations for different network requests but maintain consistent interface between.
What should the protocol include? That ultimately depends on the semantics of the model, but as a role of thumb cancellation and progress monitoring are a good candidates. Of course an object that actually implements the protocol will be needed. If the protocol only includes cancel
(which has been true for most cases I’ve used this pattern) then it’s simplest to use either an NSOperation
subclass or NSProgress
(which is a very useful but overlooked recent addition to Foundation). A category without an implementation can be used to state that these existing classes conforms to protocol. For example:
@protocol BECProgress
-(void)cancel;
@end
//Instances of NSOperation can now be used as id.
//No @implemention is required because NSOperation already implements all the methods in BECProgress.
@interface NSOperation
@end
didSucceed
: By explicitly stating whether the request succeed we avoid ambiguity. For example, is it consider a failure if result is nil
or if error
is non-nil
? What if we were able to fetch some results from one data source but another failed thus meaning we would provide results and an error? By explicitly stating the success status we avoid all of these ambiguities. Explicitly stating success is also adhering to the DRY principle because each completion handler will need to perform this check anyway.
results
: This is entirely dependant on the specifics of your model. It may be that you don’t have a result object to return and so can omit this parameter, or it may be that it makes sense to return multiple parameters.
error
: It’s worth remember that NSError
is more that just a jumped-up NSString
. Of specific interest to this situation is NSUnderlyingErrorKey
which is a standardised way by which a semantically richer description can be passed to the receiver without loosing the original error.
Two final notes:
To avoid potential concurrency issues completionHandler
should always be executed on the main thread. Your spidey senses should tingle whenever a controller method is called from anything other than the main thread.
Even if the request can be fulfilled synchronously (e.g. from a local cache) then completionHandler
should still be executed asynchronously. This is because implementations of completionHandler
will have been created with the expectation that they will be executed asynchronously and synchronous execution would thus cause undesirable things to happen.
The approach described above is fine for user-instigated requests but falls short when we want the model to instigate the request without intervention by a controller. An example of this would be synching news items in an RSS reader app. In this example the model should know when it’s time to sync but we also want the to allow the user to manually trigger the synching. Here’s an example interface:
@interface BEMNewsItemStore : NSObject
@property(nonatomic, readonly) BOOL isSynchingNewsItems;
@property(nonatomic, readonly) NSArray *newsItems;
@property(nonatomic, readonly) NSDate *newsItemsLastSynched
@property(nonatomic, readonly) NSError *newsItemsSynchError;
-(void)updateNewItems; //For manually synching
The properties are closely related to the parameters of completionHandler
:
isSynchingNewsItems
: This flag has no explicit counter part in completionHandler
(it’s implicitly YES
between the time that the request is made and the time completionHandler
is invoked). I’ve listed this as a BOOL
but it could be changed to a enum
that describes various different states.
newsItems
: This is analogous to results
(this could be replaced with NSFetchedResultsController
).
newsItemsLastSynched
: This is similar to didSucceed
but by providing date we give more information at no extra cost.
newsItemsSynchError
: This is analogous to error
.
The properties can be observer via KVO (or using your KVO wrapper of choice). In practice only isSynchingNewsItems
will likely be needed to be observed because the other properties will only ever change when synching occurs. Why KVO and not blocks, delegation or notifications?
Blocks: Blocks are well suited for transient, one off events. They are not well suited to open ended communications. This is because of their memory management behaviour which can easily lead to retain cycles that are difficult to spot.
Delegation: Delegation is a one-to-one pattern therefore it would not be possible to notify multiple objects (e.g. 2 view controller) about the change using delegation. Also, delegation is primarily an alternative to subclassing. It allows the behaviour of an instance to be customised which is not a feature we desire.
Notifications: One of the benefits of notification is loosing coupling. It means that disparate parts of the object graph can communication without an explicit edge between then. But notifications are only one way. This means that unless the receiver has an explicit references to the poster the receiver can only inspect the properties when a notification is posted. But storing an explicit reference undermines the usefulness of the loose coupling provided by notifications. In our case we need to inspect the object at times other than when a notification is posted therefore we will need an explicit reference and therefore notifications are not appropriate.
I’ve described this pattern with reference to network requests but this approach is equally as useful for any asynchronous task.