Monday, June 21, 2010

DIY-DI Objective-C

This post is an Objective-C adaptation of Do it your self Dependency Injection (Java) .The article is a very well done and comprehensive introduction for the manual DI. I find that it also explains in a simple and clear manner DI concepts. What I have done here is to apply the idea of manual DI for objective-C. For a better understanding of the DI concepts I recommend the resource section of this website.

The mini project which I am going to describe is an Objective-C iPhone project. As it uses blocks to implement providers, the compiler must support blocks. In iOS, blocks are supported starting with version 4. For previous versions of iOS plblocks must be downloaded and installed. (Note: It is possible to use other techniques to implement providers).

The following post is a step by step guide for almost trivial app. It is a Master-Details app with a persons list and a detail page. The complete source code of the example can be downloaded from (url to come....)

Application Scope

First step is the create the ApplicationScope class which will hold all objects with the lifetime of a singleton plus the configuration params.



1
2
3
4
5
@interface AppScope : NSObject {
NSDictionary *launchOptions_;
}
-(id) initWithOptions:(NSDictionary *)launchOptions;
@end


The injector

The injector will evolve as the application grows and as more code gets written. To start with the following will suffice:





1
2
3
4
@interface LCInjector : NSObject {
}
+(ListController*) injectListController:(AppScope*)appScope;
@end

For the moment this simple implementation will work just fine:


1
2
3
4
5
+(ListController*) injectListController:(AppScope*)appScope{
  ListController * listController =
    [[ListController alloc] initWithNibName:@"ListController" bundle:nil];
  return [listController autorelease];
}

We can observe that the injector provides a bunch of class methods which take a scope object as a parameter. Each method is responsible of wiring up and providing a certain object.

Injector Bootstrap

In a DI application the injector gets wired once and forgotten. Normally we would like to bootstrap the injector as early as possible, ideally in main(). As the iphone application has a particular startup sequence and for this example we don't want to change it too much, I will bootstrap the injector in appdelegate's application:didFinishLaunchingWithOptions:  This creates a couple of particularities if compared with a classical DI solution.


1
2
3
4
5
6
7
8
9
10
- (BOOL)application:(UIApplication *)application 
               didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
  AppScope *appScope = 
    [[[AppScope alloc] initWithOptions:launchOptions] autorelease];
  ListController *listController = [LCInjector injectListController:appScope];
  [navigationController pushViewController:listController animated:NO];
  [window addSubview:[navigationController view]];
  [window makeKeyAndVisible];
  return YES;
}

The main xib file contains the main window and a main navigation controller. Usually main xib files are loaded by the framework. As the UINavigationController cannot be subclassed, I don't see any problem in leaving it inside the xib. A good practice is not to keep controller or model objects inside xib files ( even if it is technically possible ).


As seen in the code, a root controller object get's pushed in the navigation controller. This controller object is obtained from the injector :

ListController *listController = [LCInjector injectListController:appScope];

and pushed it to the navigation controller:

[navigationController pushViewController:listController animated:NO];


There are a couple of important points about this method :
  • at the time of injection our app delegate object is already living. In order to set up his dependencies we should use setter injection (self.property = [LCInjector injectProperty:appScope];
  • we should forget our injector object and not keep references to it into the delegate.
  • as always it is better if we can limit the role of the app's delegate object
  • the app delegate object depends on the injector which makes it hard to test. If we limit the amount of work in application:didFinishLaunchingWithOptions: to an absolute minimum and if we limit the role of the delegate testing should become easy.
  • last but not least, if we absolutely must have objects inside main xib, objects which are needed by the injector, we should pass them to the AppScope object constructor before injection.


How to inject a siple UIViewController based class

In this trivial example we will inject a simple view controller object. The initializer of the ListController will evolve as we discover more dependencies, but the following code is ready to use template for controller injection:



1
2
3
4
5
+(ListController*) injectListController:(AppScope*)appScope{
  ListController * listController =
    [[ListController alloc] initWithNibName:@"ListController" bundle:nil];
  return [listController autorelease];
}


If we are using multiple views for the same controller (very rare case on iOS) we can also inject the xib name.

Injecting an object's  dependencies

For the example to work the list controller must display a collection of Persons objects. The re-factored initializer for ListControler, at this point is :


1
2
3
4
5
6
7
8
9
-(id) initWithNibName:(NSString *)nibNameOrNil 
               bundle:(NSBundle *)nibBundleOrNil 
              persons:(NSArray*)personsArray{
  self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
  if (self) {
    persons_ = [personsArray retain];
  }
  return self;
}

As at the creation of ListController the injector must supply an array of persons, the injector needs a method which provides that:




1
2
3
4
5
6
7
8
+(NSArray*) injectPersons:(AppScope*)appScope{
  Person *first,*seccond,*third;
  first = [Person pesrsonWithName:@"First Person" address:@"First Address"];
  seccond = [Person pesrsonWithName:@"Seccond Person" address:@"Seccond Address"];
  third = [Person pesrsonWithName:@"Third Person" address:@"Third Address"];
  NSArray * persons = [NSArray arrayWithObjects:first,seccond,third,nil];
  return persons;
}

The new injectListController is :

1
2
3
4
5
6
7
8
+(ListController*) injectListController:(AppScope*)appScope{
  NSArray *personsArray = [LCInjector injectPersons:appScope];
  ListController * listController =
    [[ListController alloc] initWithNibName:@"ListController" 
                                     bundle:nil
                                    persons:personsArray];
  return [listController autorelease]
}
On line [2] the dependency is obtained from the injector and passed to the initializer on line [6].

At this point, with a little extra code, the app looks like this :

Providers

When we click on one of the persons in this list, the app should display a detail page, where the user can eventually edit the person or view more details. To keep things simple I have created a viewcontroller which looks like this :

In order to support this behavior, the initializer of DetailController should take a Person as the argument:

1
2
3
4
5
6
7
8
9
-(id) initWithNibName:(NSString *)nibNameOrNil 
               bundle:(NSBundle *)nibBundleOrNil 
               person:(Person*) aPersonOrNil{
  self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
  if (self) {
    person_ = [aPersonOrNil retain];
  }
  return self;
}
The DetailController displays the Person's details during ViewDidLoad:

1
2
3
4
5
- (void)viewDidLoad {
  [super viewDidLoad];
  nameLabel_.text = person_.name;
  addressLabel_.text = person_.address;
}

The usual way to wire-up  this detail page is inside ListControllers tableView:didSelectRowAtIndexPath:

1
2
3
4
5
6
7
8
9
10
- (void)tableView:(UITableView *)tableView 
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  Person *selectedPerson = [persons_ objectAtIndex:indexPath.row];

  DetailController *detailController =
    [[DetailController alloc] initWithNibName:@"DetailController" 
                                       bundle:nil
                                       person:selectedPerson];
  [self.navigationController pushViewController:detailController animated:YES];
  [detailController release];
}

The ListController creates a DetailController which displays the person's details. This means that the DetailController dependency is not properly injected by the injector.  If we use the previous method to inject the DetailController inside ListController we will end up with one instance of DetailControler. The problem is that usually a new instance of DetailController is needed at each tap. The solution is to inject a provider of DetailControllers which gets called when the DetailController is needed. A simple provider implemented using blocks looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
typedef DetailController* (^DetailControllerProvider)(Person*);

+(DetailControllerProvider) injectDetailControllerProvider:(AppScope*)appScope{
  DetailControllerProvider provider =  ^DetailController*(Person* aPersonOrNil){
    DetailController *controller = 
      [[DetailController alloc] initWithNibName:@"DetailController" 
                                         bundle:nil 
                                         person:aPersonOrNil];
    return [controller autorelease];
  };
  return provider;
}


What it does is to return a block which takes a Person as parameter and returns a configured DetailController when invoked. It is worthwhile noting that the block is in the context of the injectDetailControllerProvider and it has access to appScope. It can use appScope object to inject other objets if needed. A more detailed example will follow soon.

Next step is to add a block instance variable(detailControllerProvider_) to ListController and accept the provider block as a constructor parameter:

1
2
3
4
5
6
7
8
9
10
@interface ListController : UITableViewController {
  NSArray *persons_;
  DetailController* (^detailControllerProvider_)(Person*);
}
-(id) initWithNibName:(NSString *)nibNameOrNil 
               bundle:(NSBundle *)nibBundleOrNil 
              persons:(NSArray*)personsArray
detailControllerProvider:(DetailController* (^)(Person*))provider;

@end

The ListController initializer implementation looks like this :

1
2
3
4
5
6
7
8
9
10
11
-(id) initWithNibName:(NSString *)nibNameOrNil 
               bundle:(NSBundle *)nibBundleOrNil 
              persons:(NSArray*)personsArray
detailControllerProvider:(DetailController* (^)(Person*))provider{
  self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
  if (self) {
    persons_ = [personsArray retain];
    detailControllerProvider_ = [provider retain];
  }
  return self;
}

As the blocks follow memory management rules we retain the block and release it in dealloc. After reimplementation the ListControllers tableView:didSelectRowAtIndexPath: method is:

1
2
3
4
5
6
7
- (void)tableView:(UITableView *)tableView 
   didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  Person *selectedPerson = [persons_ objectAtIndex:indexPath.row];

  DetailController *detailController = detailControllerProvider_(selectedPerson);
  [self.navigationController pushViewController:detailController animated:YES];
}

Last step is the injector's injectListController to pass the provider as initialization parameter for ListController:

1
2
3
4
5
6
7
8
9
10
11
+(ListController*) injectListController:(AppScope*)appScope{
  NSArray *personsArray = [LCInjector injectPersons:appScope];
  DetailControllerProvider provider = 
    [LCInjector injectDetailControllerProvider:appScope];
  ListController * listController =
    [[ListController alloc] initWithNibName:@"ListController" 
                                     bundle:nil
                                    persons:personsArray
                   detailControllerProvider:provider];
  return [listController autorelease];
}


1 comment:

  1. Hi, there is nice implementation of 'inversion of control' for iPhone at http://github.com/mivasi/Objective-IOC

    ReplyDelete