views:

350

answers:

3

I'm looking for a reliable design for handling assignments that have asynchronous requests involved. To further clarify, I have a class which handles Data Management. It is a singleton and contains a lot of top level data for me which is used throughout my iPhone application.

A view controller might do something such as the following:

users = [MySingleton sharedInstance].users;

MySingleton will then override the synthesized users getter and see if it is set. If it is not set, it will speak to a Connection Manager (a wrapper for NSURLConnection and its delegate methods) which fires off an asynchronous request, and this is where problems begin. I cannot guarantee when "users" will be available. I could change the request to synchronous, but that will directly effect user experience, especially in a mobile environment where bandwidth is limited already.

I need to be able to at some point, have some kind of locking/synchronization code going on in my getter that doesn't return users until it is available or is nil.

Once the NSURLConnection has the data available, it needs to callback something/somewhere with a response object and let the getter know the data is available.. whether it's failed or succeeded.

Any suggestions on handling this?

+1  A: 

You can either use a delegate pattern or a notification pattern here.

A delegate would let a particular object know when users is complete, a notification pattern would notify any object that wants to know. Both are valid, depending on your situation.

Just remember: if you have any race issues in your app, your architecture is probably all wrong.

August
The question here is, how do I delay the assignment until a notification or delegate method has been invoked?
Coocoo4Cocoa
+3  A: 

I solved this problem a couple ways in different apps.

One solution is to pass an object and selector along to notify such as:

- (id)getUsersAndNotifyObject:(id)object selector:(SEL)selector

This breaks the nice property behavior however. If you want to keep the methods as properties, have them return immediately, with either cached data or nil. If you need to go out to the network, do so asynchronous and then let the rest of the app know the data changed via KVO or the NSNotificationCenter. (Cocoa Bindings would be an option on the Mac, but they don't exist on iPhone).

The two methods are fairly similar. Register for updates with your shared instance, and then ask for the data. KVO is a little lighter weight if you just dealing with raw observable properties, but an NSNotification might be more convenient if you're interested in several different pieces of data.

With an NSNotification, the client object could register for one type of notification which includes the changed data in its userInfo dictionary instead of having to register obvservers for every single key path you're interested in.

An NSNotification would also allow you to pass back failures or other status information a lot more easily than straight KVO.

KVO method:

// register observer first so you don't miss an update
[[MySingleton sharedInstance] addObserver:self
                               forKeyPath:@"users"
                                  options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
                                  context:&kvo_users_context];
 users = [MySingleton sharedInstance].users;

 // implement appropriate observeValueForKeyPath:ofObject:change:context: method

NSNotification Method:

 [[NSNotificationCenter defaultCenter] addObserver:self
                                          selector:@selector(sharedDataChanged:)
                                              name:MySingletonDataUpdatedNotification
                                            object:[MySingletonDataUpdatedNotification sharedInstance]];
 users = [MySingleton sharedInstance].users;

 // implement appropriate sharedDataChanged: method
amrox
This is a great start, thanks for the info. My only problem is if I return nil from a property method -- how do I know the difference whether it's nil because there really is no data for that property or that it just hasn't fetched it yet? Might have zero users or haven't fetched them yet, both nil.
Coocoo4Cocoa
distinguish between nil and NSNull
Graham Lee
A: 

It took me a while to realize what the best way of handling this kind of typical task; it turns out the clue is in the design of many of Cocoa and CocoaTouch's own APIs: delegation.

The reason so many of Cocoa's APIs use delegation is because it fits very well with the asynchronous nature of many GUI apps.

It seems perfectly normal to want do do something along the lines of:

users = [MyDataFactory getUsers];

Except, as you point out, you have no idea when the getUsers method will finish. Now, there are some light-weight solutions to this; amrox mentioned a few in his post above (personally I'd say notifications aren't such a good fit but the object:selector: pattern is reasonable), but if you are doing this kind of thing a lot the delegation pattern tends to yield a more elegant solution.

I'll try to explain by way of an example of how I do things in my application.

Let's say we have a domain class, Recipe. Recipes are fetched from a web service. I typically have a series of repository classes, one for each entity in my model. A repository class' responsibility is to fetch the data required for the entity (or a collection of them), use that data to construct the objects, and then pass those objects onto something else to make use of them (typically a controller or data source).

My RecipeRepository interface might look something like this:

@interface RecipeRepository {}
- (void)initWithDelegate:(id)aDelegate;
- (void)findAllRecipes;
- (void)findRecipeById:(NSUInteger)anId;
@end

I'd then define a protocol for my delegate; now, this can be done as an informal or formal protocol, there are pros and cons of each approach that aren't relevant to this answer. I'll go with a formal approach:

@protocol RepositoryDelegateProtocol
- (void)repository:(id)repository didRetrieveEntityCollection:(NSArray *)collection;
- (void)repository:(id)repository didRetrieveEntity:(id)entity;
@end

You'll notice I've gone for a generic approach; you will likely have multiple XXXRepository classes in your app and each will use the same protocol (you may also choose to extract a base EntityRepository class that encapsulates some common logic).

Now, to use this in a controller, for example, where you previous would have done something such as:

- (void)viewDidLoad 
{
  self.users = [MySingleton getUsers];
  [self.view setNeedsDisplay];
}

You would do something like this:

- (void)viewDidLoad 
{
  if(self.repository == nil) { // just some simple lazy loading, we only need one repository instance
    self.repository = [[[RecipeRepository alloc] initWithDelegate:self] autorelease];
  }
  [self.repository findAllRecipes];
}

- (void)repository:(id)repository didRetrieveEntityCollection:(NSArray *)collection;
{
  self.users = collection;
  [self.view setNeedsDisplay];
}

You could even extend this further to display some kind of "loading" notice with an additional delegate method:

@protocol RepositoryDelegateProtocol
- (void)repositoryWillLoadEntities:(id)repository;
@end

// in your controller

- (void)repositoryWillLoadEntities:(id)repository;
{
  [self showLoadingView]; // etc.
}

Another thing about this design is that your repository classes really don't need to be singletons - they can be instantiated wherever you need them. They may deal with some kind of singleton connection manager but at this layer of abstraction a singleton is unnecessary (and its always good to avoid singletons where possible).

There is a downside to this approach; you may find you need layers of delegation at each level. For instance, your repositories may interact with some kind of connection object which does the actual asynchronous data loading; the repository might interact with the connection object using it's own delegation protocol.

As a result you might find you have to "bubble up" these delegation events throughout the different layers of your application using delegates that get more and more coarse-grained as they get closer to your application-level code. This can create a layer of indirection that can make your code harder to follow.

Anyway, this is my first answer on SO, I hope its been helpful.

Luke Redpath