Skip to content

1.x_Assembling components with blocks

Jasper Blues edited this page Apr 26, 2014 · 5 revisions

Block Assembly | Xml Assembly | 1.x_Autowiring | 1.x_Using-assembled-components | 1.x_Incorporating | 1.x_Configuration-Management-&-Testing

#News

We will be releasing Typhoon 2.0 in the coming weeks, whereupon we will archive this document. Typhoon 2.0 includes some minor changes to the block-style assembly - making it more compact and adding powerful new features.

If you wish to try Typhoon 2.0 features, please use the code in master. Otherwise checkout a 1.x tag from github or resolve from CocoaPods.

#Quick Start

Setting up a Dependency Injection container couldn't be more easy.

First, create a sub-class of TyphoonAssembly as follows:

@interface MiddleAgesAssembly : TyphoonAssembly

- (id)basicKnight;

- (id)cavalryMan;

- (id)defaultQuest;

@end

Add the method names in the header. This will allow compile-time checking and IDE code-completion. Now simply define the components:


###Perform an Initializer Injection

- (id)basicKnight
{
    return [TyphoonDefinition withClass:[Knight class] initialization:^(TyphoonInitializer* initializer)
    {
        initializer.selector = @selector(initWithQuest:);
        //For more control, you can use injectParameterAtIndex or withName, but probably just. . . 
        [initializer injectWithDefinition:[self defaultQuest]];
        //. . . which means the order will follow that of the parameters in the selector. 
    }];
}

- (id)defaultQuest
{
    return [TyphoonDefinition withClass:[CampaignQuest class]];
}

Notice how you get code-completion on the selector name, and components to be injected. So if you rename a class or method, those updates will show-up here too.

Typhoon-appcode

###Resolve the component from the container as follows:

TyphoonComponentFactory* factory = [[TyphoonBlockComponentFactory alloc] initWithAssemblies:@[
    [MiddleAgesAssembly assembly]
]];
  
Knight* knight = [(MiddleAgesAssembly*) factory basicKnight]; //Code-completion + no 'magic strings'

And we're done!


##More Details

###Injection can be done via a property setter:

  • Properties can be injected by explicit reference, or by matching the required type.
- (id)cavalryMan
{
    return [TyphoonDefinition withClass:[CavalryMan class] properties:^(TyphoonDefinition* definition)
    {
        //wire-by type
        [definition injectProperty:@selector(quest)]; 

        //explicit wiring - useful when there's more than one component representing a given type
        [definition injectProperty:@selector(quest) withDefinition:[self defaultQuest]]; 
    }];
}

Collections

A property of type NSSet or NSArray can be populated as follows:

definition injectProperty:@selector(favoriteDamsels) asCollection:^(TyphoonPropertyInjectedAsCollection* collection)
        {
            //Uses Typhoon's type converter system
            [collection addItemWithText:@"Mary" requiredType:[NSString class]];
            [collection addItemWithText:@"Mary" requiredType:[NSString class]];
            //Injects with a reference to another component
            [collection addItemWithDefinition:[self anotherKnight]];
        }];

Circular Dependencies

Sometimes you wish to define components that depend on each other. For example a ViewController that is injected with a view, and a View that is injected with the ViewController as a delegate.

This is possible using property-style injection, but not for initializer injection.

Circular Dependencies



###Additional configuration can be added in the properties block:

####Setting the scope:

  • The default scope is TyphoonScopeObjectGraph - a scope unique among DI containers. This scope is especially geared towards mobile and desktop applications. When a component is resolved, any dependencies with the object-graph will be treated as shared instances during resolution. Once resolution is complete they are not retained by the TyphoonComponentFactory. This allows instantiating an entire object graph for a use-case (say for a ViewController), and then discarding it when that use-case has completed.
  • TyphoonScopePrototype means that a new instance will always be created by Typhoon.
  • The TyphoonScopeSingleton scope means that Typhoon will retain the instance that exists for as long as the TyphoonComponentFactory exists.
  • TyphoonScopeWeakSingleton works the same as singleton, except that not components are currently using the singleton, it will be destroyed. A subsequent request will have it created again.
- (id)basicKnight
{
    return [TyphoonDefinition withClass:[Knight class] initialization:^(TyphoonInitializer* initializer)
    {
        initializer.selector = @selector(initWithQuest:);
        [initializer injectWithDefinition:[self defaultQuest]];
    } properties:^(TyphoonDefinition* definition)
    {
        [definition setScope:TyphoonScopeSingleton];
    }];
}

Property injection call-backs:

The container provides a way to invoke a method before or after property injection, to ensure that the instance is in the required state before receiving collaborating classes.

For example, in the case of a RootViewController, you can assert that root controller's view is not nil, before injecting child view controllers.

[definition setAfterPropertyInjection:@selector(configureBeforeUse)];

As an alternative to declaring the property injection methods in the assembly, if you're not worried about your class having a direct dependency on Typhoon, you can also implement the following protocol:

@protocol TyphoonPropertyInjectionDelegate <NSObject>



##Injection Styles

In the above examples, you saw some different styles of dependency injection.

By Type

Injection can be done by matching the required-type. (Guice-style). For these simple cases auto-wiring is also a good option: Autowiring

By Reference

Injection can be done by reference. This is useful in the common requirement you have multiple components matching the same class or protocol. The injection is done by referencing a method in the assembly. This allows you to easily change the name of components as your design evolves, without anything breaking.

By Value

Injection can be done by value.

  • An object instance can be provided (with auto-boxing for primitives).
  • Alternatively, a text-representation can be provided. The container will look up the required class or primitive type and do a type conversion from the string-value at run-time. It's easy to register your own additional converters.

Examples:

//A vanila case
[definition injectProperty:@selector(serviceUrl) withObjectInstance:[NSURL URLWithString:@"http://www.myapp.com/service"]]; 

//Look-up a value using a property placeholder configurer (see instructions in Config Managment & Testing)
[definition injectProperty:@selector(serviceUrl) withValueAsText:@"${client.serviceUrl}"]; 

//Use auto-boxing
[initializer injectWithObjectInstance:@(NSPrivateQueueConcurrencyType)]; 

//Inject a class
[initializer injectWithObjectInstance:[SomeClass class]]; 

//Inject a struct
[definition injectProperty:@selector(frame) withObjectInstance:[NSValue valueWithPointer:CGRectMake(10, 10, 100, 100]];



##Creating a component that instantiates other components

Sometimes its necessary to have a component that creates other components. For example a legacy singleton that produces objects you'd like to inject into other classes in your app.

- (id)swordFactory
{
    return [TyphoonDefinition withClass:[SwordFactory class]];
}

- (id)blueSword
{
    return [TyphoonDefinition withClass:[Sword class] initialization:^(TyphoonInitializer* initializer)
    {
        initializer.selector = @selector(swordWithSpecification:);
        //Specify type, because Obj-c runtime doesn't provide type-introspection for initializers
        [initializer injectParameterNamed:@"specification" withValueAsText:@"blue" requiredTypeOrNil:[NSString class]]; 
    } properties:^(TyphoonDefinition* definition)
    {
        definition.factory = [self swordFactory];
    }];
}



Modularization of Assemblies

Let's say we have an assembly where some of the components will conform to a protocol. At at run-time concrete realization of these protocols will be different depending on, for example, Production vs Test environments.

  • Typhoon allows you to group related components together under a TyphoonAssembly sub-class.
  • Typhoon allows you to extract a protocol or define a base-class for components that will change depending on circumstances.

Here's an example:

We create an assembly that’s going to use the interface from another assembly

@interface UIAssembly : TyphoonAssembly

//Typhoon will fill these in at runtime
@property(nonatomic, strong, readonly) VBNetworkComponents* networkComponents;
@property(nonatomic, strong, readonly) VBPersistenceComponents* persistenceComponents;

- (id)rootViewController;

- (id)signUpViewController;

- (id)storeViewController;

@end
@implementation UIAssembly


- (id)rootViewController
{
    return [TyphoonDefinition withClass:[RootViewController class] properties:^(TyphoonDefinition* definition)
    {
        definition.scope = TyphoonScopeSingleton;
    }];
}


- (id)signUpViewController
{
    return [TyphoonDefinition withClass:[SignUpViewController class] initialization:^(TyphoonInitializer* initializer)
    {
        initializer.selector = @selector(initWithClient:view:);
        [initializer injectWithDefinition:[_networkComponents signUpClient]];
        [initializer injectWithDefinition:[self signUpView]];
    }];
}

- (id)signUpView
{
    return [TyphoonDefinition withClass:[SignUpView class]];
}


- (id)storeViewController
{
    //etc. . . 
}

@end

Now we provide a realization of these components as follows:

@implementation NetworkComponents


- (id)signUpClient
{
    return [TyphoonDefinition withClass:[SignUpClientDefaultImpl class] properties:^(TyphoonDefinition* definition)
    {
        definition.parent = [self abstractClient];
    }];
}

- (id)storeClient
{
    return [TyphoonDefinition withClass:[StoreClientDefaultImpl class] properties:^(TyphoonDefinition* definition)
    {
        definition.parent = [self abstractClient];
        [definition injectProperty:@selector(storeDao) withDefinition:[_persistenceComponents storeDao]];
    }];
}


- (id)abstractClient
{
    return [TyphoonDefinition withClass:[ClientBase class] properties:^(TyphoonDefinition* definition)
    {
        [definition injectProperty:@selector(serviceUrl) withValueAsText:@"${client.serviceUrl}"];
        [definition injectProperty:@selector(networkMonitor) withDefinition:[self networkMonitor]];
        [definition injectProperty:@selector(allowInvalidSSLCertificates) withValueAsText:@"${client.allowInvalidSSLCertificates}"];
        [definition injectProperty:@selector(logRequests) withValueAsText:@"${client.logRequests}"];
        [definition injectProperty:@selector(logResponses) withValueAsText:@"${client.logResponses}"];
    }];
}

- (id)networkMonitor
{
    return [TyphoonDefinition withClass:[Reachability class] initialization:^(TyphoonInitializer* initializer)
    {
        initializer.selector = @selector(reachabilityForInternetConnection);
    }];
}


@end

Now we can create a container as follows:

TyphoonComponentFactory* factory = [[TyphoonBlockComponentFactory alloc] initWithAssemblies:@[
        [UIAssembly assembly],
        [NetworkComponents assembly],
        [PersistenceComponents assembly]
    ]];
  
StoreViewController* viewController = [(UIAssembly*) factory storeViewController];


#Summary

  • Block-style assembly is a great way of providing powerful dependency injection features at the same time as taking advantage of all of your IDEs code-completion, validation and refactoring tools.
  • Guice (and Spring)-style auto-wiring is supported, for simple cases. For more complex cases this style of assembly is recommended.


#Where to now?

githalytics.com alpha